-
[Next.js] Route, Pages and Layouts, Linking and Navigation, Route GroupsWeb 2023. 10. 25. 12:19
Defining Routes
https://nextjs.org/docs/app/building-your-application/routing/defining-routes
Routing: Defining Routes | Next.js
nextjs.org
Creating Routes
Next.js는 폴더를 라우트로 정의하는 파일시스템 기반의 라우터를 사용한다.
각 폴더는 라우트 세그먼트를 나타내며, URL의 각 세그먼트에 대응한다. 중첩 라우트를 생성하려면 폴더들을 중첩시키면 된다.
page.js 파일은 공개적으로 접근 가능한 라우트 세그먼트를 만드는데 사용된다
위 예에서, /dashboard/analytics URL 경로는 공개적으로 접근이 불가능한데, 이는 page.js 파일을 갖지 않기 떄문이다. 이 폴더는 컴포넌트, 스타일시트, 이미지 등을 저장하는데 쓰일 수 있다.
Creating UI
스페셜 파일 컨벤션은 각 라우트 세그먼트에 대한 UI를 생성하는데 쓰인다. 가장 흔히 사용되는 것은 어떤 라우트에 대한 유일한 UI를 보여주는 page, 그리고 다수의 라우트에 대해 공유되는 UI를 제공하는 layout이다.
예를 들어, 다음과 같은 리액트 컴포넌트를 포함하는 page.js를 app 디렉토리 내부에 추가함으로써 간단한 페이지를 만들어볼 수 있다.
export default function Page() { return <h1>Hello, Next.js!</h1> }
Pages and Layouts
https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts
Routing: Pages and Layouts | Next.js
nextjs.org
Next.js 13버전의 App Router(이하 앱 라우터)는 페이지, 공유 레이아웃, 템플릿을 쉽게 생성하기 위한 파일 컨벤션을 제공한다.
Pages
페이지는 어떠한 라우트에 대해 유일한 UI이다. 페이지를 정의하려면 page.js파일에서 컴포넌트를 export하면 된다. 폴더를 중첩시켜서 라우트를 정의하고, 이 폴더에 page.js를 생성하여 해당 라우트에 공개적인 접근이 가능하도록 할 수 있다.
// `app/page.tsx` is the UI for the `/` URL export default function Page() { return <h1>Hello, Home page!</h1> }
// `app/dashboard/page.tsx` is the UI for the `/dashboard` URL export default function Page() { return <h1>Hello, Dashboard Page!</h1> }
더보기알면 좋은 것
- Page는 항상 라우트 서브트리의 leaf이다.
- Page에는 .js, .jsx, .tsx 파일 확장자를 사용할 수 있다.
- page.js 파일은 라우트 세그먼트에 대한 공개적 접근이 가능하게 만든다.
- Page는 기본적(default)으로 서버컴포넌트이나, 클라이언트 컴포넌트로 설정될 수도 있다.
- Page는 데이터를 fetch할 수 있다.
Layouts
레이아웃이란 복수의 페이지들 간 공유되는 UI이다. navigation 시, 레이아웃은 상태를 보존하며, 상호작용 가능하고, 리렌더링되지 않는다. 레이아웃 또한 중첩될 수 있다.
레이아웃을 정의하려면 layout.js파일에서 리액트 컴포넌트를 default exporting하면 된다. 이 컴포넌트는 렌더링 중에 하위 레이아웃(있는 경우) 또는 하위 페이지로 채워질 children prop을 받아들여야 한다.
export default function DashboardLayout({ children, // will be a page or nested layout }: { children: React.ReactNode }) { return ( <section> {/* Include shared UI here e.g. a header or sidebar */} <nav></nav> {children} </section> ) }
더보기알면 좋은 것
- 최상위 레이아웃은 Root Layout이라고 불린다. 이는 어플리케이션의 모든 페이지에 걸쳐 공유되며, 반드시 html, body 태그를 포함해야 한다.
- 모든 라우트 세그먼트에 대해 이것만의 layout을 정의할 수 있다(optional). 이러한 레이아웃은 해당 세그먼트의 모든 페이지에서 공유된다.
- 라우트의 레이아웃들은 기본적으로 중첩된다(default). 각 부모 레이아웃은 리액트의 children prop을 통해 자식 레이아웃을 감싼다.
- Route Groups를 사용하여 공유 레이아웃에 특정 라우트 세그먼트를 선택적으로 포함시키거나 제외시킬 수 있다.
- 레이아웃은 default로 서버 컴포넌트이며, 클라이언트 컴포넌트로 세팅할 수도 있다.
- 레이아웃은 데이터를 fetch할 수 있다.
- 부모 레이아웃과 그의 자식들 사이에 데이터를 전달하는 것은 불가능하다. 하지만, 같은 데이터를 한 라우트에서 여러 번 가져올 수 있으며, React는 성능에 영향을 미치지 않고 요청을 자동으로 중복 제거한다.
- 레이아웃은 하위의 라우트 세그먼트에 대한 접근 권한이 없다. useSelectedLayoutSegment 혹은 useSelectedLayoutSegments를 클라이언트 컴포넌트 안에서 사용함으로서 모든 라우트 세그먼트에 접근할 수 있다.
- layout.js와 page.js 파일이 한 폴더 안에 있다면 레이아웃이 페이지를 감싼다.
Root Layout (Required)
루트 레이아웃은 app의 최상위 레벨에 정의되며 모든 라우트에 적용된다. 이를 통해 서버에서 리턴되는 최초의 HTML을 수정할 수 있다.
export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }
더보기- app 디렉토리는 반드시 root layout을 포함해야 한다.
- root layout은 반드시 <html>, <body> 태그를 포함해야 한다(Next.js가 자동으로 생성해주지 않는다).
- <head> HTML 요소를 관리하기 위해 built-in SEO support를 사용할 수 있다.
- Route groups를 통해 복수의 root layout을 생성할 수 있다. 예시
Nesting Layouts
폴더에 정의된 레이아웃은 특정 라우트 세그먼트에 적용되며, 해당 세그먼트가 active할 때 렌더링된다. 기본값으로 파일 계층구조에서 레이아웃은 중첩되며, 이는 자식 레이아웃을 부모 레이아웃이 children prop을 통해 감싼다는 것을 의미한다.
export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return <section>{children}</section> }
더보기- root layout만이 <html>, <body> 태그를 가질 수 있다.
Templates
템플릿은 부모 레이아웃이 자식 레이아웃 혹은 페이지를 감싼다는 것에서 레이아웃과 비슷하다. 라우트들에 걸쳐 지속되고 상태를 유지하는 레이아웃과 달리, 템플릿은 navigation 시 자식 각각에 대해 새 인스턴스를 생성한다. 이는 유저가 템플릿을 공유하는 라우트들을 탐색할 때, 컴포넌트의 새로운 인스턴스가 마운트되고, DOM 요소가 재생성되며, 상태는 보존되지 않으며, effect는 재동기화됨을 의미한다.
예를 들어, 다음과 같은 상황에서는 레이아웃보다 템플릿이 적합하다.
더보기- useEffect와 useState에 의존하는 기능
- 프레임워크의 기본 동작을 바꾸고자 할 때. 예를 들어, 레이아웃 내의 Suspense Boundaries는 레이아웃이 처음 로드될 때만 폴백(fallback)을 보여주고 페이지를 전환할 때는 보여주지 않는다. 템플릿의 경우, 각 탐색마다 폴백이 표시된다.
템플릿은 template.js 파일로 정의하면 된다. 역시, children prop을 받아야 한다.
export default function Template({ children }: { children: React.ReactNode }) { return <div>{children}</div> }
중첩 시, 템플릿은 레이아웃과 이것의 자식 사이에서 렌더링된다.
<Layout> {/* Note that the template is given a unique key. */} <Template key={routeParam}>{children}</Template> </Layout>
Modyfying <head>
app 디렉토리 안에서, 내장 SEO 지원을 통해 <head> HTML 요소를 조작할 수 있다.
메타데이터는 metadata 객체나, generateMetadata 함수를 layout.js나 page.js 에서 export 하여 정의할 수 있다
import { Metadata } from 'next' export const metadata: Metadata = { title: 'Next.js', } export default function Page() { return '...' }
더보기루트 레이아웃에 수동으로 <title>이나 <meta>와 같은 <head> 태그를 추가하지 않는게 좋다. 대신, 스트리밍 및 `<head>` 요소의 중복 제거와 같은 고급 요구 사항을 자동으로 처리하는 메타데이터 API를 사용하는 것이 좋다.
Linking and Navigating
Next.js에서 navigation하는 방법에는 <Link> 컴포넌트와 useRouter() 훅 두 가지가 있다.
<Link> 컴포넌트
<Link>는 빌트인 컴포넌트로서, HTML의 <a> 태그를 확장하여 prefetching과 클라이언트사이드 navigation을 제공한다.
import Link from 'next/link' export default function Page() { return <Link href="/dashboard">Dashboard</Link> }
<Link> 컴포넌트로 넘겨줄 수 있는 prop은 다음과 같다(API 레퍼런스).
예시
1. 동적 세그먼트에 대한 링킹
// app/blog/PostList.js import Link from 'next/link' export default function PostList({ posts }) { return ( <ul> {posts.map((post) => ( <li key={post.id}> <Link href={`/blog/${post.slug}`}>{post.title}</Link> </li> ))} </ul> ) }
2. 활성화된 링크 체크
// app/components/links.tsx 'use client' import { usePathname } from 'next/navigation' import Link from 'next/link' export function Links() { const pathname = usePathname() return ( <nav> <ul> <li> <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/"> Home </Link> </li> <li> <Link className={`link ${pathname === '/about' ? 'active' : ''}`} href="/about" > About </Link> </li> </ul> </nav> ) }
usePathname()을 통해 현재 path를 가져와서 활성화 상태의 클래스를 적용할 수 있다.
3. 스크롤하여 id로 이동하기
Next.js 앱 라우터의 기본 동작은 새로운 라우트로 이동할 때 페이지 상단으로 스크롤하거나, 뒤로 가기 및 앞으로 가기 탐색 시 스크롤 위치를 유지하는 것이다.
탐색 시 특정 ID로 스크롤하려면 URL에 # 해시 링크를 추가하거나, href prop에 해시 링크를 전달하기만 하면 된다. 이것이 가능한 이유는 `<Link>`가 `<a>` 요소로 렌더링되기 때문이다.이 동작을 비활성화하고 싶다면, <Link> 컴포넌트에 scroll={false}를 전달하거나, router.push() 또는 router.replace()에 scroll: false를 전달하면 된다.
// next/link <Link href="/dashboard" scroll={false}> Dashboard </Link>
// useRouter import { useRouter } from 'next/navigation' const router = useRouter() router.push('/dashboard', { scroll: false })
useRouter() 훅
useRouter() 을 통해 라우트를 직접 바꿀 수도 있다.
이 훅은 클라이언트 컴포넌트에서만 사용될 수 있다.
'use client' import { useRouter } from 'next/navigation' export default function Page() { const router = useRouter() return ( <button type="button" onClick={() => router.push('/dashboard')}> Dashboard </button> ) }
라우팅과 Navigation은 어떻게 동작하는가?
앱 라우터는 라우팅과 탐색에 대해 하이브리드 접근 방식을 사용한다. 서버에서는 애플리케이션 코드가 라우트 세그먼트별로 자동으로 코드 분할된다. 그리고 클라이언트에서는 Next.js가 라우트 세그먼트를 미리 가져오고 캐시한다. 이는 사용자가 새로운 라우트로 이동할 때 브라우저가 페이지를 다시 로드하지 않고, 변경되는 라우트 세그먼트만 다시 렌더링한다는 것을 의미하고, 이로써 탐색 경험과 성능이 향상된다.
1. Prefetch(미리 가져오기)
prefetch는 유저의 방문 전에 백그라운드에 라우트를 미리 로드해놓는 것을 말한다.
더보기<Link> 컴포넌트: 라우트들은 유저의 뷰포트에 보여질 경우 자동적으로 prefetch 된다.
router.prefetch(): useRouter 훅을 통해서도 할 수 있다.
<Link> 컴포넌트를 통한 prefetch는 정적인 라우트와 동적인 라우트에 대해 다르게 행동한다.
더보기정적 라우트: prefetch의 기본값은 true이다. 전체 라우트가 prefetch되고 캐싱된다.
동적 라우트: prefetch의 기본값은 자동이다. 공유 레이아웃부터 첫 loading.js 파일까지만 30초 동안 prefetch 및 캐싱된다. 이는 전체 동적 라우트를 가져오는 비용을 줄이고, 사용자에게 더 나은 시각적 피드백을 위한 즉각적인 로딩 상태를 보여줄 수 있음을 의미한다.prefetch 프롭을 false로 설정하여 prefetching을 비활성화할 수도 있다.
2. 캐싱
Next.js는 라우터 캐시라 불리는 in-memory 클라이언트 사이드 캐시를 가지고 있다. 유저가 앱 내부를 탐색할 때, prefetch된 라우트 세그먼트의 React 서버 컴포넌트 페이로드와 방문한 라우트는 캐시에 저장된다.
이는 탐색 시, 서버에 대한 새로운 요청 없이 캐시는 최대한으로 재사용되어 성능이 최적화됨을 의미한다.
3. 부분 렌더링
부분 렌더링은 navigation 시 바뀐 라우트 세그먼트만 클라이언트에서 재렌더링되고, 공유 세그먼트는 보존되는 것을 말한다.
예를 들어, 두 형제 라우트인 /dashboard/settings 와 /dashboard/analytics 간 navigation 시, 이들만 재렌더링되고 dashboard 레이아웃은 보존된다.
부분적 재렌더링 없이는 navigation 시 마다 전체 페이지가 서버에서 다시 렌더링될 것이다.
4. Soft Navigation
기본값으로, 브라우저는 페이지 간 hard navigation을 수행한다. 이는 브라우저가 페이지를 리로드하고, React state를 초기화하며, Browser state(유저의 스크롤 위치나 focused element 등)또한 초기화됨을 말한다. 그러나 Next.js의 앱 라우터는 soft navigation을 사용한다. 이는 React가 React / browser state를 보존하며, 바뀐 세그먼트만 렌더링함을 의미하고, 전체 페이지를 새로고침하지 않음을 의미한다.
5. Back and Forward Navigation
기본값으로, Next.js는 앞/뒤 탐색 시 스크롤 위치는 유지하며, Router Cache의 라우트 세그먼트를 재사용한다.
Route Groups
app 디렉토리 안에서, 중첩된 폴더들은 보통 URL 경로로 매핑된다. 그러나, 폴더를 라우트 그룹으로 지정하여 해당 폴더가 URL 경로에 포함되는 것을 막을 수도 있다.
이를 통해 URL 경로 구조에 영향을 미치지 않고, 라우트 세그먼트와 프로젝트 파일들을 논리적 그룹으로 만들 수도 있다.
라우트 그룹은 다음과 같은 경우에 유용하다.
- 라우트들을 그룹으로 묶어서 구성하고 싶은 경우
- 같은 라우트 세그먼트 수준에서 중첩 레이아웃을 활성화하고 싶은 경우
- 여러 루트 레이아웃을 포함하여 같은 세그먼트에 여러 중첩 레이아웃을 만들고 싶은 경우
- 공통 세그먼트의 라우트 하위 집합에 레이아웃을 추가하고 싶은 경우
Convention
라우트 그룹은 폴더 이름을 소괄호로 감싸서 만들 수 있다.
Examples
라우트 그룹 구성하기
위와 같은 경우에서, (marketing)과 (shop) 은 같은 URL 계층 구조를 따름에도, 아래와 같이 각각에 layout.js를 추가하여 다른 레이아웃을 적용할 수 있다.
특정 세그먼트를 레이아웃에 포함시키기
특정 라우트에 대해서만 라우트 그룹으로 묶어 원하는 레이아웃을 적용할 수 있다.
다수의 root 레이아웃 적용하기
이 때, <html>, <body> 태그는 각 레이아웃에 모두 포함되어야 한다.
Dynamic Routes
세그먼트의 이름을 미리 특정할 수 없고, 동적 데이터로부터 라우트를 생성하고 싶을 때, 요청 시 채워지거나 빌드 타임에 사전 렌더링되는 동적 세그먼트(dynamic segment)를 사용할 수 있다.
Convention
동적 세그먼트는 폴더 이름을 [folderName] 과 같이 대괄호로 감싸면 된다.
동적 세그먼트는 param 이라는 prop으로 layout, page, route, generateMetadata 함수에 전달된다.
Example
// app/blog/[slug]/page.js export default function Page({ params }: { params: { slug: string } }) { return <div>My Post: {params.slug}</div> }
Generating Static Params
generateStaticParams 함수는 요청 시 on-demand로 생성하는 대신, 빌드 시간에 라우트를 정적으로 생성하기 위해 동적 라우트 세그먼트와 함께 사용될 수 있다.
// app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await fetch('https://.../posts').then((res) => res.json()) return posts.map((post) => ({ slug: post.slug, })) }
generateStaticParams 함수의 주요 이점은 데이터를 스마트하게 가져온다는 것이다. generateStaticParams 함수 내에서 fetch 요청을 사용하여 콘텐츠를 가져오는 경우, 요청은 자동으로 memoization된다. 이는 여러 generateStaticParams, 레이아웃, 그리고 페이지들에 걸쳐 동일한 인수를 가진 fetch 요청이 단 한 번만 이루어진다는 것을 의미하며, 이로 인해 빌드 시간이 단축된다.
(generateStaticParams API reference)
Catch-all Segments
동적 세그먼트는 연속적인 catch-all 세그먼트들로 확장될 수 있다.
예를 들어, app/shop/[...slug]/page.js 는 /shop/clothes에 매칭될 수 있고, 또한 /shop/clothes/tops, /shop/clothes/tops/t-shirts 에도 매칭될 수 있다.
Optional Catch-all Segments
대괄호를 한 번 더 씌워서 optional하게 catch-all 세그먼트를 만들 수도 있다.
일반적인 catch-all 세그먼트와의 차이는, 파라미터가 없을 때에도 라우트가 매치된다는(/shop)것이다.
TypeScript
params의 타입은 다음과 같이 지정할 수 있다.
// app/blog/[slug]/page.tsx export default function Page({ params }: { params: { slug: string } }) { return <h1>My Page</h1> }
'Web' 카테고리의 다른 글
[Next.js] Routing Fundamentals (0) 2023.10.24 [Next.js] App Router (0) 2023.10.24 [Next.js] Pages Router (0) 2023.10.22 [Next.js] introduction (0) 2023.10.22