레이아웃과 페이지
레이아웃과 페이지 (Layouts and Pages)
Next.js의 App Router는 폴더와 파일이 곧 라우트가 되는 파일 시스템 기반 라우팅을 써요. app 폴더 안에 page 파일을 만들면 그 경로가 화면이 되고, layout 파일을 만들면 여러 화면이 공유하는 골격이 생기죠. 이 글에서는 페이지와 레이아웃을 만들고, 그 사이를 오가는 기본기를 차근차근 살펴볼게요.
페이지 만들기
페이지(page) 는 특정 경로(route)에서 렌더링되는 UI예요. app 폴더 안에 page 파일을 만들고 React 컴포넌트를 기본(default)으로 내보내면 되는데, 예를 들어 홈(/)에 해당하는 인덱스 페이지는 이렇게 만들 수 있어요.
export default function Page() {
return <h1>Hello Next.js!</h1>
}
레이아웃 만들기
레이아웃(layout) 은 여러 페이지가 함께 공유하는 UI예요. 네비게이션을 오가는 동안 레이아웃은 상태를 유지하고, 계속 인터랙티브하며, 다시 렌더링되지 않는다는 특징이 있어요.
레이아웃은 layout 파일에서 컴포넌트를 기본으로 내보내 정의하며, children prop을 받아요. children 자리에는 페이지나 또 다른 (중첩된) 레이아웃이 들어올 수 있어요.
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* Layout UI */}
<main>{children}</main>
</body>
</html>
)
}
app 폴더 최상단에 정의된 레이아웃은 루트 레이아웃(root layout) 이라고 불러요. 루트 레이아웃은 필수이며, html과 body 태그를 반드시 포함해야 해요.
중첩 라우트 만들기
중첩 라우트(nested route)는 여러 URL 세그먼트로 이루어진 라우트예요. 예를 들어 /blog/[slug]는 세 개의 세그먼트로 나뉘죠.
/(루트 세그먼트)blog(세그먼트)[slug](리프 세그먼트)
Next.js에서는 폴더가 URL 세그먼트에 대응하고, 파일(page, layout 등)이 해당 세그먼트에 보여줄 UI를 만든다고 기억하면 돼요. 폴더를 중첩하면 그만큼 라우트가 깊어지고, /blog를 외부에 공개하려면 blog 폴더 안에 page 파일을 추가하면 돼요.
대괄호로 폴더 이름을 감싸면(예: [slug]) 동적 라우트 세그먼트가 돼요. 블로그 글이라든가 상품 페이지처럼 데이터로부터 여러 페이지를 만들어 낼 때 사용해요.
레이아웃 중첩하기
폴더 계층에 있는 레이아웃은 기본적으로 함께 중첩돼요. 즉, 상위 레이아웃이 children prop을 통해 하위 레이아웃을 감싸는 구조예요. 라우트 세그먼트에 해당하는 폴더 안에 layout 파일을 추가하면 중첩 레이아웃을 만들 수 있어요.
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)을 감싸는 형태가 돼요.
동적 세그먼트 만들기
동적 세그먼트(dynamic segment)는 데이터로부터 라우트를 생성하게 해 줘요. 블로그 글마다 라우트를 일일이 만들 필요 없이, [segmentName]처럼 폴더 이름을 대괄호로 감싸면 데이터 기반으로 라우트를 만들 수 있어요.
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>
)
}
검색 파라미터로 렌더링하기
서버 컴포넌트 페이지에서는 searchParams prop으로 검색 파라미터에 접근할 수 있어요.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = (await searchParams).filters
}
searchParams를 사용하면 요청이 들어와야 파라미터를 읽을 수 있기 때문에 페이지가 동적 렌더링(dynamic rendering) 으로 전환돼요. 클라이언트 컴포넌트에서는 useSearchParams 훅으로 검색 파라미터를 읽을 수 있어요.
무엇을 언제 쓸지 정리하면 이래요.
- 페이지의 데이터 로딩에 검색 파라미터가 필요하면
searchParamsprop을 써요 (예: 페이지네이션, DB 필터링). - 검색 파라미터가 클라이언트에서만 쓰이면(예: 이미 props로 불러온 목록 필터링)
useSearchParams를 써요. - 콜백이나 이벤트 핸들러 안에서는
new URLSearchParams(window.location.search)로 읽으면 리렌더링을 피하는 작은 최적화가 돼요.
페이지 사이를 오가기 (링크)
next/link에서 제공하는 <Link> 컴포넌트로 라우트 사이를 이동할 수 있어요. <Link>는 HTML <a> 태그를 확장한 내장 컴포넌트로, 프리페칭(prefetching)과 클라이언트 사이드 네비게이션까지 기본 제공해요.
import Link from 'next/link'
import { getPosts } from '@/lib/posts'
export default async function Posts() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
알아두면 좋아요:
<Link>는 Next.js에서 라우트를 오가는 기본 방식이에요. 더 고급스러운 네비게이션이 필요하면useRouter훅도 사용할 수 있어요.
라우트 Props 헬퍼
Next.js는 라우트 구조로부터 params와 명명된 슬롯(named slot)을 추론하는 유틸리티 타입을 제공해요.
PageProps—params와searchParams를 포함한page컴포넌트용 PropsLayoutProps—children과 명명된 슬롯(예:@analytics같은 폴더)을 포함한layout컴포넌트용 Props
이 헬퍼들은 next dev, next build 또는 next typegen을 실행할 때 생성되는 전역 헬퍼라서, 별도 import가 필요 없어요.
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
return <h1>Blog post: {slug}</h1>
}
더 알아보기 (Learn more)
- Linking and Navigating — 프리페칭, 프리렌더링, 클라이언트 사이드 네비게이션 등 내장 네비게이션 최적화 이해하기
- layout.js — layout 파일 API 레퍼런스
- page.js — page 파일 API 레퍼런스
- Link Component —
next/link의 빠른 클라이언트 사이드 네비게이션 - Dynamic Segments — 동적 라우트로 URL 경로 파라미터 읽기와 동적 데이터 기반 라우트 생성