iBetter Books
수정

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은 그대로 유지하는 기법입니다.