Next.js에서 동적 라우팅은 게시글 상세 페이지, 상품 상세 페이지, 사용자 프로필 페이지처럼 URL의 일부가 데이터에 따라 달라지는 페이지를 만들 때 사용합니다. 예를 들어 /posts/1, /posts/2, /products/macbook처럼 주소는 다르지만 같은 화면 구조를 재사용해야 하는 경우가 대표적입니다.
Next.js App Router에서는 app 디렉터리 안에 대괄호 폴더를 만들면 동적 라우트를 구현할 수 있습니다. 공식 문서에서도 동적 세그먼트는 폴더명을 [folderName] 형태로 감싸서 만든다고 설명합니다. 예를 들어 app/blog/[slug]/page.tsx는 /blog/hello-nextjs, /blog/dynamic-routing 같은 URL을 처리할 수 있습니다.
기본 동적 라우트 만들기
먼저 게시글 상세 페이지를 예로 들어보겠습니다.
// app/posts/[id]/page.tsx
type PostDetailPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function PostDetailPage({
params,
}: PostDetailPageProps) {
const { id } = await params;
return (
<main>
<h1>게시글 상세 페이지</h1>
<p>현재 게시글 ID: {id}</p>
</main>
);
}
위 구조에서 [id]는 URL에서 변하는 값을 의미합니다.
/posts/1 → id = "1"
/posts/25 → id = "25"
/posts/hello → id = "hello"
즉, 파일을 여러 개 만들지 않아도 하나의 page.tsx로 여러 상세 페이지를 처리할 수 있습니다.
실제 데이터 가져오기
실제 서비스에서는 URL의 id를 이용해 DB나 API에서 데이터를 조회합니다.
// app/posts/[id]/page.tsx
import { notFound } from "next/navigation";
type Post = {
id: string;
title: string;
content: string;
};
async function getPost(id: string): Promise<Post | null> {
const posts: Post[] = [
{
id: "1",
title: "Next.js 시작하기",
content: "Next.js는 React 기반의 풀스택 프레임워크입니다.",
},
{
id: "2",
title: "동적 라우팅 이해하기",
content: "동적 라우팅은 URL에 따라 다른 데이터를 보여줍니다.",
},
];
return posts.find((post) => post.id === id) ?? null;
}
type PostDetailPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function PostDetailPage({
params,
}: PostDetailPageProps) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
notFound();
}
return (
<main>
<h1>{post.title}</h1>
<p>{post.content}</p>
</main>
);
}
여기서 중요한 부분은 notFound()입니다. 존재하지 않는 게시글 ID로 접근했을 때 직접 에러 화면을 만들 필요 없이 Next.js의 404 페이지로 이동시킬 수 있습니다.
slug 기반 라우팅
게시글이나 상품 페이지에서는 숫자 ID보다 slug를 자주 사용합니다. slug는 사람이 읽기 쉬운 URL 문자열입니다.
/posts/nextjs-dynamic-routing
/products/iphone-16-pro
구조는 다음과 같이 만들 수 있습니다.
app
└─ posts
└─ [slug]
└─ page.tsx
// app/posts/[slug]/page.tsx
type PostPageProps = {
params: Promise<{
slug: string;
}>;
};
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
return (
<main>
<h1>게시글 상세</h1>
<p>현재 slug: {slug}</p>
</main>
);
}
이 방식은 블로그, 뉴스, 상품 상세 페이지에 적합합니다. URL이 검색 엔진과 사용자 모두에게 더 명확하게 보이기 때문입니다.
여러 단계의 동적 라우팅
동적 라우트는 한 단계만 가능한 것이 아닙니다. 예를 들어 도시별 관광지 상세 페이지를 만든다면 다음과 같이 구성할 수 있습니다.
app
└─ tour
└─ [city]
└─ [spotId]
└─ page.tsx
// app/tour/[city]/[spotId]/page.tsx
type TourSpotPageProps = {
params: Promise<{
city: string;
spotId: string;
}>;
};
export default async function TourSpotPage({
params,
}: TourSpotPageProps) {
const { city, spotId } = await params;
return (
<main>
<h1>관광지 상세 페이지</h1>
<p>도시: {city}</p>
<p>관광지 ID: {spotId}</p>
</main>
);
}
이 경우 URL은 다음과 같이 동작합니다.
/tour/seoul/1
/tour/busan/3
/tour/jeju/10
이런 구조는 카테고리, 지역, 상품군처럼 계층형 데이터를 다룰 때 유용합니다.
generateStaticParams로 정적 생성하기
동적 라우트는 기본적으로 요청 시점에 처리할 수도 있지만, 미리 생성할 수 있는 페이지라면 generateStaticParams를 사용할 수 있습니다. 공식 문서에 따르면 generateStaticParams는 동적 라우트와 함께 사용하여 요청 시점이 아니라 빌드 시점에 경로를 정적으로 생성할 수 있습니다.
// app/posts/[slug]/page.tsx
const posts = [
{
slug: "nextjs-start",
title: "Next.js 시작하기",
},
{
slug: "dynamic-routing",
title: "동적 라우팅 이해하기",
},
];
export async function generateStaticParams() {
return posts.map((post) => ({
slug: post.slug,
}));
}
type PostPageProps = {
params: Promise<{
slug: string;
}>;
};
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
const post = posts.find((post) => post.slug === slug);
if (!post) {
return <div>게시글을 찾을 수 없습니다.</div>;
}
return (
<main>
<h1>{post.title}</h1>
<p>현재 slug: {slug}</p>
</main>
);
}
generateStaticParams는 블로그 글, 상품 상세, 문서 페이지처럼 데이터가 자주 바뀌지 않고 SEO가 중요한 페이지에 적합합니다.
Link로 동적 페이지 이동하기
동적 라우트를 만들었다면 목록 페이지에서 상세 페이지로 이동할 수 있어야 합니다.
// app/posts/page.tsx
import Link from "next/link";
const posts = [
{
id: "1",
title: "Next.js 시작하기",
},
{
id: "2",
title: "동적 라우팅 이해하기",
},
];
export default function PostsPage() {
return (
<main>
<h1>게시글 목록</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/posts/${post.id}`}>
{post.title}
</Link>
</li>
))}
</ul>
</main>
);
}
Link 컴포넌트를 사용하면 페이지 전체를 새로고침하지 않고 Next.js의 클라이언트 라우팅 방식으로 자연스럽게 이동할 수 있습니다.
Next.js 동적 라우팅의 핵심은 폴더 이름에 대괄호를 사용하는 것입니다. [id], [slug]는 하나의 동적 값, [...slug]는 여러 단계의 경로, [[...slug]]는 기본 경로까지 포함하는 선택적 경로를 처리합니다. 여기에 params를 이용해 URL 값을 가져오고, 필요하면 generateStaticParams로 정적 페이지를 미리 생성할 수 있습니다.
초보자라면 먼저 /posts/[id] 형태의 게시글 상세 페이지를 만들어보는 것이 좋습니다. 이후 slug, 중첩 라우트, Catch-all 라우트 순서로 확장하면 Next.js의 동적 라우팅 구조를 자연스럽게 이해할 수 있습니다.