동적 라우트 — [slug]로 무한한 URL 처리하기

Next.js App Router의 동적 라우트 세그먼트를 완전히 이해합니다. [param] 문법, params 비동기 접근, 중첩 동적 세그먼트, generateStaticParams를 이용한 정적 생성까지 다룹니다.

· 4 min read · PALDYN Team

지난 글에서 서버·클라이언트 컴포넌트를 올바르게 합성하는 방법을 배웠습니다. 이번에는 URL의 일부를 변수처럼 처리하는 동적 라우트를 살펴봅니다. 블로그 포스트, 상품 상세, 사용자 프로필처럼 수백·수천 개의 URL을 하나의 파일로 처리할 때 필수입니다.

[param] 문법

대괄호로 폴더 이름을 감싸면 동적 세그먼트가 됩니다. [slug]/blog/hello-world에서 hello-world 부분을 캡처합니다.

동적 라우트 params 흐름

app/
└── blog/
    ├── page.tsx          → /blog
    └── [slug]/
        └── page.tsx      → /blog/[모든값]

params는 비동기입니다 (Next.js 15+)

Next.js 15부터 paramsPromise로 변경됐습니다. await로 unwrap해야 합니다.

// app/blog/[slug]/page.tsx
type Props = {
  params: Promise<{ slug: string }>;
};

export default async function BlogPost({ params }: Props) {
  const { slug } = await params; // ✅ await 필수
  const post = await getPost(slug);

  if (!post) notFound(); // 404 처리

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

중첩 동적 세그먼트

동적 라우트 파일 구조와 URL 매핑

여러 단계의 동적 세그먼트를 중첩할 수 있습니다. params에 모든 세그먼트가 포함됩니다.

// app/shop/[category]/[id]/page.tsx
type Props = {
  params: Promise<{ category: string; id: string }>;
};

export default async function ProductPage({ params }: Props) {
  const { category, id } = await params;
  // category = 'electronics', id = '42'
  const product = await getProduct(category, id);
  return <ProductDetail product={product} />;
}

params의 값은 항상 string입니다. 숫자 ID가 필요하면 Number(id) 또는 parseInt(id, 10)으로 변환하세요.

generateStaticParams — 빌드 타임 사전 생성

동적 라우트 페이지를 빌드 시 미리 생성하려면 generateStaticParams를 export합니다.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPosts();

  return posts.map((post) => ({
    slug: post.slug, // { slug: 'hello-world' }, { slug: 'nextjs-tips' }, ...
  }));
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{post.content}</article>;
}

generateStaticParams가 반환하지 않은 slug로 접근하면 기본적으로 동적 렌더링이 됩니다. dynamicParams = false를 설정하면 404를 반환합니다.

// 목록에 없는 slug → 404
export const dynamicParams = false;

searchParams — 쿼리 파라미터 접근

URL의 쿼리 파라미터(?page=2&sort=asc)는 searchParams로 접근합니다. 이것도 Next.js 15에서 Promise가 됐습니다.

// /blog?page=2&sort=asc
type Props = {
  searchParams: Promise<{ page?: string; sort?: string }>;
};

export default async function BlogListPage({ searchParams }: Props) {
  const { page = '1', sort = 'desc' } = await searchParams;
  const posts = await getPosts({ page: Number(page), sort });
  return <PostList posts={posts} />;
}

params는 폴더 구조에서 오고, searchParams는 URL 쿼리 문자열에서 옵니다. 두 가지 모두 Page 컴포넌트에서만 접근할 수 있습니다(Layout에서는 불가).


지난 글: 서버·클라이언트 컴포넌트 합성 패턴

다음 글: Catch-all 라우트 — 가변 경로 세그먼트 처리


읽어주셔서 감사합니다. 😊