Next.js 레이아웃과 페이지 (Layouts and Pages)
Next.js 공식 문서 Getting Started: Layouts and Pages 내용을 바탕으로, App Router 기준의 레이아웃·페이지 개념을 정리한 자료입니다.
1. 페이지 생성 (Creating a page)
- **페이지(page)**는 특정 경로(route)에 대응하는 UI입니다.
app디렉터리 안에 **page.tsx파일을 만들고 기본 내보내기(default export)**로 React 컴포넌트를 작성합니다.- 예: 루트 경로(
/)에 대응하는 페이지
// app/page.tsx
export default function Page() {
return <h1>Hello Next.js!</h1>
}
정리:
app/경로/page.tsx파일이 있으면 해당 경로가 “공개 라우트”가 됩니다.
2. 레이아웃 생성 (Creating a layout)
- **레이아웃(layout)**은 여러 페이지에서 공유되는 UI입니다.
- 페이지 간 이동 시:
- 레이아웃은 상태(state)를 유지하고,
- 인터랙티브한 상태를 유지하며,
- 불필요하게 다시 렌더링되지 않습니다.
layout파일에서 기본 내보내기하는 React 컴포넌트가 레이아웃이 되며,childrenprops를 받아 그 위치에 하위 페이지/레이아웃을 렌더링합니다.
// app/layout.tsx (루트 레이아웃 예시)
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* 공통 레이아웃 UI */}
<main>{children}</main>
</body>
</html>
)
}
2.1 루트 레이아웃 (Root layout)
app디렉터리 **최상단에 있는app/layout.tsx**를 **루트 레이아웃(root layout)**이라고 부릅니다.- 루트 레이아웃은 필수이며, 반드시
<html>과<body>태그를 포함해야 합니다. - 전역 폰트, 전역 스타일, 공통 헤더/푸터 등을 여기에서 설정합니다.
3. 중첩 라우트 생성 (Creating a nested route)
**중첩 라우트(nested route)**는 여러 개의 URL 세그먼트로 이루어진 경로입니다.
예: /blog/[slug] 는 다음 세 부분으로 구성됩니다.
/: 루트 세그먼트blog: 중간 세그먼트[slug]: 말단(leaf) 세그먼트
Next.js App Router에서:
- 폴더는 URL 세그먼트와 1:1로 매핑됩니다.
- **파일(
page,layout등)**은 해당 세그먼트에서 실제로 렌더링되는 UI를 정의합니다.
3.1 /blog 라우트 만들기
app/
blog/
page.tsx
// app/blog/page.tsx
import { getPosts } from '@/lib/posts'
import { Post } from '@/ui/post'
export default async function Page() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</ul>
)
}
app/blog/page.tsx→ 경로:/blog
3.2 /blog/[slug] 라우트 만들기
app/
blog/
page.tsx
[slug]/
page.tsx
// app/blog/[slug]/page.tsx
export default function Page() {
return <h1>Hello, Blog Post Page!</h1>
}
app/blog/[slug]/page.tsx→ 경로 예시:/blog/my-post
[slug]와 같이 폴더 이름을 대괄호로 감싸면 **동적 라우트 세그먼트(dynamic route segment)**가 됩니다.
4. 레이아웃 중첩 (Nesting layouts)
레이아웃은 폴더 구조에 따라 자동으로 중첩됩니다.
즉, 상위 폴더의 레이아웃이 하위 폴더의 레이아웃·페이지를 children으로 감싸는 구조입니다.
4.1 /blog 전용 레이아웃 만들기
app/
layout.tsx // 루트 레이아웃
blog/
layout.tsx // 블로그 전용 레이아웃
page.tsx
[slug]/
page.tsx
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
- 실제 렌더링 계층은 다음과 같이 구성됩니다.
app/layout.tsx(루트 레이아웃)app/blog/layout.tsx(블로그 레이아웃)app/blog/page.tsx또는app/blog/[slug]/page.tsx(페이지)
정리: 상위 폴더에
layout.tsx가 있으면, 그 아래 폴더들에 대한 공통 레이아웃이 됩니다.
5. 동적 세그먼트 생성 (Creating a dynamic segment)
**동적 세그먼트(dynamic segment)**는 데이터 기반으로 여러 페이지를 자동 생성할 때 사용합니다. 예: 블로그 글 목록, 상품 상세 페이지 등.
- 폴더 이름을
[segmentName]형태로 작성합니다. - 예:
app/blog/[slug]/page.tsx에서[slug]가 동적 세그먼트입니다.
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
)
}
params에는 URL 세그먼트의 값이 담기며, 타입은Promise<{ slug: string }>형태로 정의되어 있습니다.- 동적 세그먼트와
params에 대한 더 자세한 내용은 Dynamic Segments 문서에서 다룹니다.
또한, 동적 세그먼트 안에 중첩된 레이아웃도 params에 접근할 수 있습니다.
6. searchParams로 렌더링 (Rendering with search params)
Server Component 페이지에서는 searchParams props를 통해 쿼리 스트링(search parameters)에 접근할 수 있습니다.
// app/page.tsx
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = (await searchParams).filters
// filters 값을 이용해 필터링, 페이징 등 처리
}
searchParams를 사용하면 **요청(request)에 따라 값을 읽어야 하므로, 해당 페이지는 동적 렌더링(dynamic rendering)**으로 분류됩니다.- 클라이언트 컴포넌트에서는
useSearchParams훅을 사용하여 검색 파라미터를 읽을 수 있습니다.
6.1 무엇을 언제 사용할까? (What to use and when)
-
searchParamsprops 사용- 검색 파라미터가 데이터 로딩에 직접 영향을 줄 때
- 예: DB 조회용 필터, 페이지네이션 등
-
useSearchParams훅 사용- 이미 props로 받아온 데이터에 대해 클라이언트에서만 필터링할 때
- 예: 정렬/필터 UI만 클라이언트에서 동작하는 경우
-
소규모 최적화 팁
- 단순히 이벤트 핸들러 내부에서만 검색 파라미터가 필요할 경우
new URLSearchParams(window.location.search)를 사용해 리렌더링 없이 값을 읽는 것도 가능
7. 페이지 간 링크 (Linking between pages)
Next.js에서는 next/link의 <Link> 컴포넌트로 경로 간 네비게이션을 구현합니다.
<Link>는 HTML<a>태그를 확장한 컴포넌트입니다.- prefetching, 클라이언트 사이드 네비게이션을 지원합니다.
// app/ui/post.tsx (예시)
import Link from 'next/link'
export default async function PostList() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
참고: 일반적인 페이지 이동은
<Link>로 처리하고, 보다 세밀한 제어가 필요할 때는useRouter훅을 사용할 수 있습니다.
8. Route Props Helpers
Next.js는 라우트 구조를 바탕으로 params, named slots 등을 타입 수준에서 자동으로 추론하는 유틸리티 타입을 제공합니다.
-
PagePropspage컴포넌트용 props 타입params,searchParams를 포함
-
LayoutPropslayout컴포넌트용 props 타입children과 병렬 라우트 슬롯(예:@analytics) 등을 포함
// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
return <h1>Blog post: {slug}</h1>
}
// app/dashboard/layout.tsx
export default function Layout(props: LayoutProps<'/dashboard'>) {
return (
<section>
{props.children}
{/* app/dashboard/@analytics 가 있다면 props.analytics 슬롯으로 접근 가능 */}
</section>
)
}
8.1 특징 정리
- 정적 라우트의
params는 자동으로{}타입이 됩니다. PageProps,LayoutProps는 **전역 타입(helper)**이므로 별도 import가 필요 없습니다.- 타입 생성 시점
next devnext buildnext typegen실행 시 자동 생성
9. 전체 흐름 요약
-
page.tsx- 특정 URL에 대응하는 페이지 UI를 정의합니다.
-
layout.tsx- 여러 페이지에서 공유하는 UI를 정의합니다.
- 폴더 구조에 따라 자동으로 중첩되며, 전역(루트) 레이아웃은 필수입니다.
-
폴더 구조
- 폴더 = URL 세그먼트
page,layout파일이 실제 렌더링되는 UI를 정의합니다.
-
동적 세그먼트 +
params[slug]형태의 폴더로 데이터 기반 라우트를 만들고,paramsprops로 세그먼트 값을 읽어옵니다.
-
searchParams/useSearchParams- 서버/클라이언트 각각에서 검색 파라미터를 다루는 방법을 제공합니다.
-
<Link>- 라우트 간 이동 시 기본적으로 사용하는 네비게이션 컴포넌트입니다.
-
Route Props Helpers
PageProps,LayoutProps로 라우트별 props 타입을 안전하게 관리할 수 있습니다.