동적 라우트 생성
대괄호 폴더로 단일·다중 동적 세그먼트를 만들고 params와 generateStaticParams로 상세 페이지를 구성합니다.
이전 장에서는 정적인 URL 경로를 가진 페이지를 만들고 연결하는 방법을 배웠습니다.
하지만 실제 서비스에서는 URL 일부가 데이터에 따라 동적으로 바뀌는 경우가 훨씬 많습니다.
예를 들어 블로그 상세(/blog/nextjs-basics),
상품 상세(/products/luxury-watch-xyz),
사용자 프로필(/users/alice) 같은 경로가 대표적입니다.
Next.js App Router는 이러한 요구사항을 충족시키기 위해 동적 라우트(Dynamic Routes) 기능을 제공합니다.
이 절에서는 동적 라우트를 생성하고, URL에서 동적인 값을 추출하여 페이지 컴포넌트에서 사용하는 방법에 대해 자세히 알아보겠습니다.
아래 표는 고정 URL과 동적 URL이 폴더 구조와 params로 어떻게 연결되는지 먼저 정리합니다.
고정 폴더는 그대로 URL이 되고, [slug] 같은 대괄호 폴더는 실제 URL 값을 받아 페이지로 전달한다.
| URL 예시 | 폴더 구조 | params 값 | 화면에서 하는 일 |
|---|---|---|---|
| /about | app/about/page.tsx | 없음 | 고정 소개 페이지 렌더 |
| /blog/nextjs-basics | app/blog/[slug]/page.tsx | { slug: 'nextjs-basics' } | slug로 게시물 조회 |
| /products/p001 | app/products/[id]/page.tsx | { id: 'p001' } | id로 상품 상세 조회 |
| /users/alice | app/users/[username]/page.tsx | { username: 'alice' } | 사용자 프로필 조회 |
| /blog/web/intro | app/blog/[category]/[slug]/page.tsx | { category, slug } | 카테고리와 글 식별자 사용 |
동적 라우트의 개념과 필요성
정적 라우트가 /about이나 /dashboard/settings처럼 고정된 URL을 가진다면, 동적 라우트는 URL의 특정 부분이 변수처럼 작동합니다.
블로그 게시물:/blog/my-first-post,/blog/nextjs-deep-dive상품 상세:/products/12345,/products/67890사용자 프로필:/users/john-doe,/users/jane-smith
이러한 URL들은 /blog/, /products/, /users/는 고정되어 있지만, 그 뒤의 my-first-post, 12345, john-doe와 같은 값은 각 항목에 따라 달라집니다.
Next.js는 이 동적인 부분을 감지하고, 해당 값을 페이지 컴포넌트로 전달하여 우리가 각기 다른 콘텐츠를 렌더링할 수 있도록 돕습니다.
동적 라우트 생성하기: 대괄호 [] 사용
아래 표는 [slug] 폴더를 만들면 URL 값이 어떤 이름의 params로 들어오는지 정리한 것입니다.
대괄호 안 이름을 바꾸면 코드에서 읽어야 하는 params 키도 같이 바뀐다.
| 만든 폴더 | 매칭 URL | page.tsx에서 읽는 값 | 주의할 점 |
|---|---|---|---|
| app/blog/[slug] | /blog/my-first-post | const { slug } = await params | slug 철자가 폴더명과 같아야 함 |
| app/products/[id] | /products/123 | const { id } = await params | id와 slug를 섞지 않음 |
| app/users/[username] | /users/alice | const { username } = await params | URL 값은 문자열로 들어옴 |
| app/blog/[category]/[slug] | /blog/web/intro | category, slug | 폴더 깊이와 URL 깊이가 일치 |
| app/blog/slug | /blog/slug | 없음 | 대괄호가 없으면 고정 경로 |
Next.js App Router에서 동적 라우트 세그먼트를 정의하려면, 폴더 이름을 대괄호([])로 감쌉니다.
이 대괄호 안의 이름이 파라미터(Parameter)의 키(key)가 됩니다.
예를 들어, 블로그 게시물 상세 페이지를 만든다고 가정해 봅시다.
src/app/blog 폴더 생성:
먼저 src/app 안에 blog 폴더를 생성합니다. (만약 없다면)
[slug] 폴더 생성:
src/app/blog 안에 [slug]라는 이름의 폴더를 생성합니다.
여기서 slug는 동적인 값이 들어갈 파라미터의 이름이 됩니다.
page.tsx 파일 생성:
src/app/blog/[slug] 폴더 안에 page.tsx 파일을 생성합니다.
src/app/blog/[slug]/page.tsx 파일 내용 작성:
이 page.tsx 컴포넌트는 params라는 prop을 통해 URL에서 추출된 동적인 값을 전달받습니다.
// src/app/blog/[slug]/page.tsx
interface BlogDetailPageProps {
params: Promise<{
slug: string; // [slug] 폴더 이름과 일치해야 합니다.
}>;
}
// 서버 컴포넌트로 작동하며, params를 prop으로 받습니다.
export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
const { slug } = await params; // URL에서 slug 값을 추출
// 실제 애플리케이션에서는 이 slug 값을 사용하여
// 데이터베이스나 API에서 해당 게시물의 내용을 불러올 것입니다.
// 예: const post = await getPostBySlug(slug);
return (
<div>
<h1>블로그 게시물: {slug}</h1>
<p>
현재 보고 계신 게시물의 식별자(slug)는 <strong>{slug}</strong> 입니다.
</p>
<p>여기에 실제 게시물 내용이 표시됩니다.</p>
</div>
);
}실습: 개발 서버가 실행 중인 상태에서 브라우저를 열고 다음 URL로 접속해 보세요.
http://localhost:3000/blog/my-first-posthttp://localhost:3000/blog/nextjs-deep-divehttp://localhost:3000/blog/welcome-to-my-blog
각 URL에 따라 페이지 제목과 내용에 slug 값이 다르게 표시되는 것을 확인할 수 있습니다.
다중 동적 라우트 세그먼트
하나의 라우트에 여러 개의 동적 세그먼트를 포함할 수도 있습니다.
예를 들어, 특정 카테고리 내의 블로그 게시물을 나타내는 /blog/[category]/[slug]와 같은 경로를 만들 수 있습니다.
src/app/blog/[category]/[slug]/page.tsx 구조 만들기src/app/blog/[category]/[slug]/page.tsx 파일 내용 작성interface CategoryBlogDetailPageProps {
params: Promise<{
category: string; // [category] 폴더 이름과 일치
slug: string; // [slug] 폴더 이름과 일치
}>;
}
export default async function CategoryBlogDetailPage({ params }: CategoryBlogDetailPageProps) {
const { category, slug } = await params;
return (
<div>
<h1>카테고리: {category}</h1>
<h2>게시물: {slug}</h2>
<p>
이 게시물은 <strong>{category}</strong> 카테고리에 속하며, 식별자는 <strong>{slug}</strong> 입니다.
</p>
</div>
);
}실습: 이제 다음 URL로 접속해 보세요.
http://localhost:3000/blog/web-dev/understanding-reacthttp://localhost:3000/blog/frontend/nextjs-features
URL의 category와 slug 부분이 정확히 추출되어 페이지에 표시되는 것을 볼 수 있습니다.
generateStaticParams 함수
동적 라우트를 가진 페이지를 정적으로 생성(Static Site Generation, SSG)해야 할 때, Next.js는 빌드 시점에 어떤 params 값을 가지고 페이지를 생성할지 알아야 합니다.
이를 위해 generateStaticParams 함수를 사용합니다.
이 함수는 서버 컴포넌트와 동일한 파일에 정의할 수 있습니다.
// ... (BlogDetailPage 컴포넌트 코드) ...
// SSG를 위해 빌드 시 어떤 slug 값을 생성할지 명시
export async function generateStaticParams() {
const posts = [
{ slug: 'my-first-post' },
{ slug: 'nextjs-deep-dive' },
{ slug: 'understanding-server-components' },
];
// 반환 값은 { slug: '...' } 형태의 객체 배열이어야 합니다.
return posts;
}generateStaticParams 함수가 정의되면, Next.js는 빌드 시 이 함수가 반환하는 slug 값들을 사용하여 미리 HTML 페이지를 생성합니다.
이는 첫 로딩 속도를 매우 빠르게 하고, CDN을 통해 콘텐츠를 효율적으로 제공할 수 있게 합니다.
동적 라우트를 설계할 때는 URL 패턴, params 키, 정적 생성 범위를 한 번에 맞춰보면 실수를 줄일 수 있습니다.
자주 바뀌지 않는 상세 페이지는 정적 생성 후보를 명시하면 빌드 시 HTML을 준비할 수 있다.
| 구성 | 예시 | 역할 | 주의할 점 |
|---|---|---|---|
| 함수 위치 | app/blog/[slug]/page.tsx | 해당 동적 라우트의 정적 후보 정의 | page 파일과 같은 세그먼트 |
| 반환 형태 | [{ slug: 'my-first-post' }] | params 객체 배열 | 폴더명과 키 이름 일치 |
| 빌드 결과 | /blog/my-first-post | 미리 생성 가능한 페이지 | 목록에 없는 값 처리 정책 필요 |
| 다중 세그먼트 | { category: 'web', slug: 'intro' } | 각 대괄호 키를 모두 제공 | 빠진 키가 있으면 매칭 불가 |
| 사용 기준 | 블로그, 문서, 상품 상세 | URL 후보가 미리 알려진 경우 | 실시간 데이터는 별도 전략 검토 |
동적 라우트는 실제 웹 애플리케이션에서 자주 필요한 기능입니다.
URL 값에 따라 페이지를 구성해야 할 때 사용합니다.
아래 다이어그램은 URL의 가변 세그먼트가 대괄호 폴더와 매칭되고, params 객체를 통해 페이지 컴포넌트로 전달되는 흐름을 정리한 것입니다.
서버 컴포넌트 page에서는 params를 await해 실제 slug나 id 값을 꺼낸다.
| 순서 | Next.js가 보는 것 | 전달되는 값 | 코드에서 확인 |
|---|---|---|---|
| 1 | 요청 URL /blog/nextjs-basics | blog + nextjs-basics | 고정 blog 뒤 가변 값 확인 |
| 2 | 폴더 app/blog/[slug] | slug 키 결정 | 대괄호 안 이름 확인 |
| 3 | page.tsx 호출 | params Promise 전달 | props 타입을 Promise로 선언 |
| 4 | await params | { slug: 'nextjs-basics' } | const { slug } = await params |
| 5 | 데이터 조회 | slug로 DB/API 조회 | 없는 slug는 notFound 처리 검토 |
아래 다이어그램은 동적 라우트를 설계할 때 URL 패턴, 폴더 이름, 정적 생성 범위를 함께 맞추는 기준을 정리합니다.
한 칸이라도 어긋나면 링크는 열려도 데이터 조회나 빌드 생성에서 문제가 생긴다.
| 설계 질문 | 좋은 기준 | 틀렸을 때 신호 | 확인 위치 |
|---|---|---|---|
| 무엇이 가변인가 | slug, id, username처럼 식별자만 대괄호 | 불필요하게 깊은 URL | 폴더 이름 |
| 키 이름이 맞는가 | [slug] → params.slug | undefined 출력 | page.tsx params 타입 |
| URL 깊이가 맞는가 | /blog/[category]/[slug] | 404 또는 잘못된 값 | 폴더 깊이와 링크 href |
| 정적으로 만들 수 있나 | generateStaticParams 후보 존재 | 빌드 누락 페이지 | 반환 객체 배열 |
| 없는 값은 어떻게 하나 | notFound 또는 fallback 정책 | 빈 화면 또는 오류 | 데이터 조회 분기 |
이 다이어그램은 동적 라우트 생성을 Next.js 프로젝트에 넣을 때 결정해야 할 파일 위치와 런타임 경계를 정리합니다.
동적 page는 기본적으로 서버 컴포넌트로 두고, params 값으로 필요한 데이터를 서버에서 조회하는 흐름이 자연스럽다.
| 결정할 것 | 권장 위치 | 좋은 신호 | 나쁜 신호 |
|---|---|---|---|
| 상세 페이지 파일 | app/blog/[slug]/page.tsx | URL 값이 params.slug로 들어옴 | 고정 slug 폴더를 만들어 버림 |
| 데이터 조회 | 동적 page 서버 컴포넌트 | slug로 DB/API 조회 | 클라이언트에서 비밀 API 호출 |
| 정적 후보 | generateStaticParams | 반환 키가 폴더명과 일치 | slug 대신 id를 반환 |
| 없는 데이터 | notFound() 처리 | 없는 slug는 404로 마감 | undefined 화면 노출 |
| 상호작용 | 작은 client component | 상세 페이지 일부만 hydrate | page 전체를 use client로 전환 |
마지막으로 동적 세그먼트와 generateStaticParams가 URL 값과 정적 생성 범위를 어떻게 나누는지 확인합니다.