Ch 04. 이름 모를 여행자를 위해 — 동적 라우팅
빈칸이 있는 주소
호텔 예약 시스템을 만든다고 합시다. 각 방마다 고유한 페이지가 있습니다. 101호, 102호, 201호... 방이 100개면 페이지도 100개입니다. 방 번호마다 파일을 하나씩 만들어야 할까요.
물론 아닙니다. "방 번호 자리에 무엇이 오든 이 페이지로 처리해"라고 선언하면 됩니다. 동적 라우팅(Dynamic Routing)이 이것을 해줍니다.
[slug] 폴더 문법
대괄호 []로 감싼 폴더명을 쓰면 동적 세그먼트가 됩니다.
app/posts/[slug]/page.tsx
이 파일은 /posts/anything, /posts/my-first-post, /posts/hello-world 등 /posts/ 뒤에 어떤 값이 오든 모두 처리합니다.
params prop으로 URL 파라미터 받기
동적 세그먼트의 값은 params prop으로 전달됩니다.
// 파일: app/posts/[slug]/page.tsx
interface Post {
slug: string;
title: string;
content: string;
author: string;
publishedAt: string;
}
interface Props {
params: Promise<{ slug: string }>;
}
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
cache: 'no-store',
});
if (!res.ok) {
throw new Error('게시글을 찾을 수 없습니다.');
}
return res.json();
}
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<p>
{post.author} · {post.publishedAt}
</p>
<div>{post.content}</div>
</article>
);
}
```text
`params`는 `Promise`로 감싸져 있으므로 `await`로 풀어서 씁니다. Next.js 15부터 `params`가 비동기가 되었습니다.
### `[...slug]` catch-all 라우트
대괄호 안에 `...`을 붙이면 여러 세그먼트를 한 번에 잡습니다.
app/docs/[...slug]/page.tsx
이 파일은 `/docs/intro`, `/docs/guide/getting-started`, `/docs/api/v1/users` 등을 모두 처리합니다. `params.slug`는 배열입니다.```tsx// 파일: app/docs/[...slug]/page.tsxinterface Props { params: Promise<{ slug: string[] }>;}export default async function DocsPage({ params }: Props) { const { slug } = await params; // slug = ['guide', 'getting-started'] (예: /docs/guide/getting-started) const path = slug.join('/'); return ( <div> <p>현재 경로: /docs/{path}</p> </div> );}```text`[[...slug]]` (이중 대괄호)를 쓰면 세그먼트가 없는 경우(`/docs`)도 처리합니다.### `generateStaticParams`로 정적 생성동적 라우트를 SSG로 처리하고 싶다면 `generateStaticParams`를 사용합니다. 빌드 시점에 어떤 경로들을 미리 만들지 알려주는 함수입니다.```tsx// 파일: app/posts/[slug]/page.tsxinterface Post { slug: string; title: string; content: string; author: string; publishedAt: string;}export async function generateStaticParams() { const posts: Post[] = await fetch('https://api.example.com/posts').then( (r) => r.json() ); return posts.map((post) => ({ slug: post.slug, }));}interface Props { params: Promise<{ slug: string }>;}async function getPost(slug: string): Promise<Post> { const res = await fetch(`https://api.example.com/posts/${slug}`, { cache: 'force-cache', }); return res.json();}export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await getPost(slug); return ( <article> <h1>{post.title}</h1> <div>{post.content}</div> </article> );}
generateStaticParams가 반환한 슬러그 목록에 해당하는 페이지들이 빌드 시점에 생성됩니다. 목록에 없는 슬러그로 접근하면 기본적으로 404를 반환하거나, dynamicParams = true(기본값)인 경우 런타임에 생성합니다.
다음 챕터에서는
다음 챕터에서는 URL에는 영향을 주지 않지만 파일을 논리적으로 그룹화하는 방법을 배웁니다. 로그인 관련 페이지를 한 폴더에 모아두면서 /login, /register라는 URL은 그대로 유지하는 기법입니다.