BlogFlow | 블로그

프론트엔드 개발과 IT 기술을 중심으로 실무 경험과 학습을 기록합니다.

Next

Next.js [slug] 동적 라우팅 사용법

2026.04.01·수정 2026.04.02·약 13분

이 글에서 정리하는 내용

Next.js App Router 기준으로 동적 세그먼트가 왜 필요한지부터, params, useParams(), generateStaticParams()를 언제 쓰는지까지 흐름에 맞춰 정리했습니다. 문법만 외우기보다 주소와 파일 구조가 어떻게 연결되는지 자연스럽게 이해하는 데 초점을 맞췄습니다.

동적 세그먼트의 의미와 필요한 이유

ChatGPT Image 2026년 4월 1일 오후 06 25 44

[slug]는 주소에서 바뀌는 부분을 코드에서 받기 위한 문법입니다. Next.js는 파일 시스템 기반 라우팅을 사용하므로, 폴더 이름을 [slug]처럼 대괄호로 감싸면 그 자리가 고정된 문자열이 아니라 바뀔 수 있는 값이 됩니다. 그래서 게시글 상세, 상품 상세처럼 화면 구조는 같지만 주소에 따라 내용만 달라지는 페이지를 만들 때 특히 많이 사용합니다. 같은 형태의 페이지를 여러 개 복사하는 대신, 하나의 규칙으로 재사용할 수 있다는 점이 핵심입니다.

파일 구조와 URL 연결

app/
  blog/ // '/blog' 경로 아래에 있는 페이지들
    [slug]/ // URL에서 바뀌는 값을 받는 동적 세그먼트 폴더
      page.tsx // 해당 slug 값을 기준으로 상세 화면을 보여주는 페이지 파일

이 구조를 볼 때는 파일 경로만 보지 말고 실제 주소를 같이 떠올리면 이해가 쉬워집니다. app/blog/[slug]/page.tsx는 결국 /blog/값 형태의 주소와 연결됩니다. 예를 들어 /blog/nextjs-slug로 들어오면 slug 값은 nextjs-slug가 되고, /blog/app-router-basics로 들어오면 app-router-basics가 됩니다. 즉 [slug]는 파일 이름이 아니라, URL 일부를 변수처럼 받아오는 자리라고 보면 됩니다.

대괄호 안 이름은 꼭 slug여야 할까?

app/blog/[slug]/page.tsx // params.slug 로 읽게 됩니다.
app/blog/[nameSlug]/page.tsx // params.nameSlug 로 읽게 됩니다.
app/product/[id]/page.tsx // params.id 로 읽게 됩니다.

이 부분은 처음 보면 꽤 헷갈릴 수 있습니다. [slug]는 고정 문법처럼 보이지만, 실제로는 예시 이름에 가깝습니다. 그래서 [nameSlug], [id], [productId]처럼 원하는 이름으로 정할 수 있고, 그 이름이 그대로 params 객체의 키가 됩니다. 예를 들어 app/blog/[nameSlug]/page.tsx라면 코드에서는 params.nameSlug로 읽게 됩니다. 다만 아무 이름이나 쓰기보다는 slug, id, categorySlug처럼 의미가 바로 보이는 이름을 쓰는 편이 읽기도 좋고 유지보수에도 유리합니다.

서버와 클라이언트에서 읽는 방법

App Router에서 동적 경로 값을 읽는 방법은 크게 두 가지입니다. 페이지처럼 서버에서 바로 렌더링되는 곳에서는 params로 받고, 클라이언트 컴포넌트에서는 useParams()로 읽습니다. 여기서 함께 자주 헷갈리는 것이 searchParams인데, 이 값은 ?page=2처럼 쿼리스트링을 읽을 때 사용합니다. 반면 동적 경로 값은 /blog/hello처럼 경로 자체에 들어 있는 값입니다. 또 폴더 이름을 [slug]로 만들면 params.slug가 되고, [nameSlug]로 만들면 params.nameSlug가 됩니다.

서버 컴포넌트에서 params 사용

type PageProps = {
  // 동적 경로 값이 params로 들어옵니다.
  params: Promise<{ slug: string }>
};

export default async function Page({ params }: PageProps) {
  // 현재 URL의 slug 값을 꺼냅니다.
  const { slug } = await params;

  return (
    <main>
      {/* 받아온 slug 값을 화면에 출력합니다. */}
      <h1>현재 글: {slug}</h1>
    </main>
  );
}

페이지 컴포넌트처럼 기본적으로 서버에서 렌더링되는 영역에서는 params를 먼저 익히는 편이 좋습니다. 상세 페이지에서 데이터를 찾거나, 권한을 확인하거나, 메타데이터를 만들 때도 이 값을 기준으로 작업하는 경우가 많기 때문입니다. App Router의 pagelayout은 기본적으로 서버 컴포넌트이므로, 실무에서도 이 흐름이 먼저 나오는 편입니다.

클라이언트 컴포넌트에서 useParams() 사용

'use client';
// 이 컴포넌트는 브라우저에서 실행됩니다.

import { useParams } from 'next/navigation';

export default function SlugViewer() {
  // 현재 URL의 동적 경로 값을 읽습니다.
  const params = useParams<{ slug: string }>();

  // 읽어온 slug 값을 화면에 보여줍니다.
  return <div>현재 slug: {params.slug}</div>;
}

반대로 버튼 클릭, 상태 관리, 브라우저 API처럼 브라우저에서만 가능한 동작이 필요한 경우에는 useParams()가 잘 맞습니다. 현재 주소의 값을 읽어서 탭을 바꾸거나, 특정 UI만 조건부로 보여주는 식의 보조 로직에 자주 사용됩니다. 다만 페이지의 핵심 데이터까지 전부 클라이언트에서 처리하기보다, 기본 데이터는 서버 컴포넌트에서 받고 상호작용만 클라이언트 컴포넌트로 내려주는 쪽이 보통 더 안정적입니다.

구분 언제 쓰는가
params page, layout, generateMetadata처럼 서버 쪽에서 slug를 받아 핵심 데이터를 처리할 때
useParams() 클라이언트 컴포넌트 안에서 현재 경로 값을 읽어 UI 상호작용에 활용할 때

블로그 상세 페이지 예시

const posts = [
  // 상세 페이지에서 찾을 예시 게시글 목록입니다.
  { slug: 'nextjs-slug', title: 'Next.js slug 정리' },
  { slug: 'app-router-basics', title: 'App Router 기초' },
];

type PageProps = {
  // URL의 slug 값을 params로 전달받습니다.
  params: Promise<{ slug: string }>
};

export default async function Page({ params }: PageProps) {
  // 현재 주소에서 slug 값을 꺼냅니다.
  const { slug } = await params;

  // slug와 일치하는 게시글 하나를 찾습니다.
  const post = posts.find((item) => item.slug === slug);

  // 일치하는 데이터가 없으면 안내 문구를 보여줍니다.
  if (!post) {
    return <main>게시글이 없습니다.</main>;
  }

  return (
    <main>
      {/* 찾은 게시글 제목을 출력합니다. */}
      <h1>{post.title}</h1>
      {/* 현재 어떤 slug로 들어왔는지도 함께 보여줍니다. */}
      <p>현재 slug: {slug}</p>
    </main>
  );
}

블로그 상세 페이지 예시는 동적 라우팅의 흐름을 가장 직관적으로 보여줍니다. 목록 화면에서는 여러 글을 보여주고, 상세 화면에서는 URL에 들어온 slug를 기준으로 딱 하나의 글을 찾습니다. 상품 상세 페이지도 거의 같은 구조입니다. 그래서 slug를 이해하면 게시글, 상품, 카테고리 페이지까지 응용 범위를 넓히기 쉬워집니다. 실무에서는 이 값이 주소에 그대로 노출되므로, 사람이 읽기 쉬운 형태로 관리하는 편이 좋습니다.

목록에서 상세 페이지로 이동할 때 연결되는 방식

import Link from 'next/link';

const posts = [
  // 목록에 표시할 예시 데이터입니다.
  { slug: 'nextjs-slug', title: 'Next.js slug 정리' },
  { slug: 'app-router-basics', title: 'App Router 기초' },
];

export default function PostList() {
  return (
    <ul>
      {posts.map((post) => (
        // 각 글의 slug를 이용해 상세 페이지 주소를 만듭니다.
        <li key={post.slug}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

이 예시를 함께 보면 흐름이 더 자연스럽게 연결됩니다. 목록 페이지에서는 Link/blog/슬러그 형태의 주소를 만들고, 상세 페이지에서는 그 값을 다시 받아 맞는 데이터를 찾습니다. 결국 링크를 만드는 쪽과 값을 읽는 쪽이 하나의 구조로 이어진다고 보면 됩니다.

확장 문법과 정적 생성

ChatGPT Image 2026년 4월 1일 오후 06 26 48
export async function generateStaticParams() {
  return [
    // 빌드할 때 미리 생성할 상세 경로 목록입니다.
    { slug: 'nextjs-slug' },
    { slug: 'app-router-basics' },
  ];
}

generateStaticParams()는 빌드할 때 미리 만들어 둘 경로 목록을 반환하는 함수입니다. 예를 들어 게시글 목록이나 상품 목록을 배포 전에 알고 있다면, 해당 slug 경로를 미리 준비해 둘 수 있습니다. 자주 접근되는 상세 페이지를 안정적으로 제공하고 싶을 때 특히 유용합니다. 반대로 어떤 값이 들어올지 미리 알 수 없는 구조라면, 모든 경로를 사전 생성하는 방식이 맞는지 먼저 검토하는 편이 좋습니다.

[slug], […slug], [[…slug]] 차이

app/blog/[slug]/page.tsx // 주소 한 칸만 받는 기본 동적 세그먼트
app/docs/[...slug]/page.tsx // 주소를 한 칸 이상 여러 개 받는 catch-all 라우트
app/shop/[[...slug]]/page.tsx // 주소가 없어도 되고, 여러 개가 와도 되는 optional catch-all 라우트

이 부분은 한 번에 외우려 하기보다, 주소 예시와 함께 보면 훨씬 쉽습니다.

[slug]는 주소 한 칸만 받습니다. 그래서 /blog/hello는 가능하지만, /blog처럼 값이 아예 없거나 /blog/a/b처럼 여러 칸이 오는 경우는 맞지 않습니다.

[...slug]는 여러 칸을 받을 수 있지만, 최소 1개는 있어야 합니다. 그래서 /docs/guide, /docs/guide/install은 가능하지만 /docs는 매칭되지 않습니다.

[[...slug]]는 가장 범위가 넓습니다. /shop처럼 아무 값이 없어도 되고, /shop/clothes, /shop/clothes/top처럼 여러 칸이 와도 됩니다.

정리하면 [slug]는 ‘한 칸’, [...slug]는 ‘최소 1개는 필요한 여러 칸’, [[...slug]]는 ‘없어도 되고, 있어도 되는 여러 칸’이라고 기억하면 됩니다.

정리

정리하면 동적 세그먼트는 주소의 바뀌는 부분을 받아 상세 데이터와 연결하는 장치입니다. App Router에서는 서버 컴포넌트의 params, 클라이언트 컴포넌트의 useParams(), 그리고 필요할 때의 generateStaticParams()를 함께 이해하면 전체 흐름이 선명해집니다. 또 대괄호 안 이름은 꼭 slug일 필요가 없고, [id], [productId], [categorySlug]처럼 의미에 맞게 정할 수 있습니다. 처음에는 문법처럼 보여도, 실제로는 상세 페이지 구조를 설계할 때 자주 만나는 핵심 개념입니다.

많이 받는 질문

Q. 동적 경로를 쓰면 실제 파일이 많이 생성되나요?
보통은 그렇지 않습니다. 기본적으로는 하나의 페이지 파일이 있고, 들어온 값에 따라 다른 데이터를 보여주는 구조입니다. 다만 generateStaticParams()를 사용하면 특정 경로를 빌드 시점에 미리 준비할 수 있습니다.

Q. slug와 query string은 같은 건가요?
같지 않습니다. slug/blog/nextjs-slug처럼 경로 일부이고, query string은 ?page=2처럼 추가 정보입니다. 하나는 동적 경로 값이고, 다른 하나는 검색 파라미터라고 보면 구분이 쉽습니다.

Q. React Router의 :id와 비슷한가요?
큰 방향은 비슷합니다. 둘 다 URL의 변하는 값을 받아 화면을 분기한다는 점에서는 같습니다. 다만 Next.js는 파일 시스템 기반 라우팅을 사용하므로, 폴더 구조로 경로를 정의한다는 차이가 있습니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기