본문으로 건너뛰기
코드룸CODE ROOMTENMO ↗

Next.js 장점을 살리는, 동적 라우팅 구현하기

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의 동적 라우팅 구조를 자연스럽게 이해할 수 있습니다.

블로그 목록으로