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이 검색 엔진과 사용자 모두에게 더 명확하게 보이기 때문입니다.
