Catch-all 세그먼트 사용
이전 절에서 동적 라우트 [slug]가
단일 파라미터 처리에 유용하다는 점을 확인했습니다.
하지만 실무에서는 URL 길이가 가변적이거나
중첩된 여러 경로를 한 페이지에서 처리해야 하는 경우가 자주 생깁니다.
예를 들어 문서 사이트의 /docs/getting-started,
/docs/api/v1/auth, /docs/features/dynamic-routes처럼
경로 깊이가 달라지는 시나리오가 대표적입니다.
이러한 시나리오를 위해 Next.js App Router는 Catch-all 세그먼트를 제공합니다. Catch-all 세그먼트는 URL의 나머지 경로 조각을 하나의 배열 파라미터로 받습니다.
Catch-all 세그먼트란?
Catch-all 세그먼트 사용에서 라우팅, 렌더링 경계, 배포 기준을 정리한 것입니다.
Catch-all 세그먼트는 폴더 이름을 세 개의 점(...)과 파라미터 이름을 함께 대괄호([])로 감싸서 정의합니다.
-
[...slug](필수 Catch-all 세그먼트): 이 형태는 해당 세그먼트가 최소한 하나 이상의 값을 포함해야 함을 의미합니다. 즉,/docs/[...slug]라면/docs/a는 매칭되지만/docs는 매칭되지 않습니다. URL에서 추출된 값은 문자열 배열로params객체에 전달됩니다. -
[[...slug]](선택적 Catch-all 세그먼트): 이 형태는 해당 세그먼트가 값이 없어도 매칭됨을 의미합니다. 즉,/docs/[[...slug]]라면/docs도 매칭되고/docs/a/b도 매칭됩니다. URL에서 추출된 값이 없을 경우params객체에서 해당 키는undefined가 되며, 값이 있을 경우[...slug]와 동일하게 문자열 배열로 전달됩니다.
이 두 가지 형태 모두 URL의 가변적인 부분을 유연하게 처리할 수 있게 해줍니다.
다음 다이어그램은 [...slug]와 [[...slug]]가 어떤 URL을 매칭하는지,
그리고 URL path 조각이 params.slug로 전달되는 흐름을 한눈에 비교합니다.
구현 전에는 "빈 경로를 허용할 것인가"와 "콘텐츠 조회 키를 어떻게 만들 것인가"를 먼저 정리해야 합니다. 아래 다이어그램은 URL이 라우트 파라미터와 콘텐츠 조회 단계로 이어지는 흐름을 분리해 보여줍니다.
필수 Catch-all 세그먼트 구현하기
필수 Catch-all 세그먼트 [...slug]는 블로그에서 카테고리와 게시물 제목 외에, 임의의 깊이를 가진 서브 카테고리를 처리하고 싶을 때 유용합니다. 예를 들어 /articles/programming/javascript/nextjs-deep-dive와 같은 경로를 처리할 수 있습니다.
우리는 /docs/path/to/document와 같은 경로를 처리하는 문서를 만들 것입니다.
src/app/docs 폴더 생성:
먼저 src/app 안에 docs 폴더를 생성합니다.
[...slug] 폴더 생성:
src/app/docs 안에 [...slug]라는 이름의 폴더를 생성합니다.
page.tsx 파일 생성:
src/app/docs/[...slug] 폴더 안에 page.tsx 파일을 생성합니다.
src/app/docs/[...slug]/page.tsx 파일 내용 작성:
interface DocDetailPageProps {
params: {
slug: string[]; // Catch-all 세그먼트는 문자열 배열로 전달됩니다.
};
}
// 이 컴포넌트는 서버 컴포넌트로 동작합니다.
export default function DocDetailPage({ params }: DocDetailPageProps) {
const { slug } = params; // URL에서 slug 값을 배열로 추출
// slug 배열을 사용하여 실제 문서 데이터를 불러오는 로직을 여기에 작성합니다.
// 예: const docContent = await fetchDocContent(slug.join('/'));
const docPath = slug.join(' / '); // 경로를 가독성 좋게 표시하기 위함
return (
<div>
<h1>문서 상세 페이지</h1>
<p>
현재 문서의 경로는 <strong>/docs/{slug.join('/')}</strong> 입니다.
</p>
<p>
추출된 파라미터 (`params.slug`): <code>{JSON.stringify(slug)}</code>
</p>
<p>여기에 실제 문서 내용이 표시됩니다.</p>
</div>
);
}
// SSG를 위해 미리 생성할 경로들을 정의합니다. (선택 사항)
export async function generateStaticParams() {
const paths = [
{ slug: ['getting-started'] },
{ slug: ['api', 'v1', 'auth'] },
{ slug: ['features', 'dynamic-routes'] },
];
return paths;
}실습:
개발 서버(npm run dev)가 실행 중인 상태에서 브라우저를 열고 다음 URL로 접속해 보세요.
http://localhost:3000/docs/first-documenthttp://localhost:3000/docs/category/sub-category/article-titlehttp://localhost:3000/docs/a/b/c/d
각 URL에 따라 params.slug가 배열 형태로 추출되고 페이지에 표시되는 것을 확인할 수 있습니다. /docs로만 접속하면 페이지를 찾을 수 없다는 404 에러가 발생할 것입니다. 이는 [...slug]가 최소한 하나의 세그먼트를 요구하기 때문입니다.
선택적 Catch-all 세그먼트 구현하기
이제 /docs 경로 자체도 매칭시키고 싶을 때 사용하는 [[...slug]]를 구현해 보겠습니다.
기존 [...slug] 폴더를 [[...slug]]로 이름 변경:
src/app/docs/[...slug] 폴더의 이름을 src/app/docs/[[...slug]]로 변경합니다. (또는 새롭게 생성합니다.)
src/app/docs/[[...slug]]/page.tsx 파일 내용 수정:
page.tsx 파일 내용은 거의 동일하지만, params.slug가 undefined일 수 있다는 점을 고려하여 코드를 수정합니다.
interface DocsPageProps {
params: {
slug?: string[]; // slug가 선택적(optional)이므로 ? 추가
};
}
export default function DocsPage({ params }: DocsPageProps) {
const { slug } = params; // slug가 없을 경우 undefined
// slug 배열이 존재하면 경로를 합치고, 없으면 "홈"으로 표시
const docPath = slug ? slug.join(' / ') : '홈';
return (
<div>
<h1>문서 페이지 ({docPath})</h1>
{slug ? (
<p>
현재 문서의 경로는 <strong>/docs/{slug.join('/')}</strong> 입니다.
</p>
) : (
<p>문서 홈 페이지입니다. 시작하려면 왼쪽 메뉴를 선택하세요.</p>
)}
<p>추출된 파라미터 (`params.slug`): <code>{JSON.stringify(slug)}</code></p>
<p>여기에 실제 문서 내용이 표시됩니다.</p>
</div>
);
}
// generateStaticParams도 마찬가지로 빈 배열을 포함할 수 있습니다.
export async function generateStaticParams() {
const paths = [
{ slug: ['getting-started'] },
{ slug: ['api', 'v1', 'auth'] },
// 루트 문서 페이지도 미리 생성하려면 빈 배열을 추가합니다.
{ slug: [] },
];
return paths;
}실습: 이제 다음 URL로 접속해 보세요.
http://localhost:3000/docs(매칭 성공!)http://localhost:3000/docs/first-documenthttp://localhost:3000/docs/category/sub-category/article-title
/docs 경로가 매칭되어 문서 홈 페이지가 표시되고, 다른 경로들도 이전처럼 잘 동작하는 것을 확인할 수 있습니다.
Catch-all 세그먼트의 활용 시나리오
- 동적인 URL 구조 처리: 문서, 블로그, 위키 등 계층적이고 유연한 URL 구조가 필요한 경우에 유용합니다.
- 파일 시스템 기반 CMS: 파일 경로 자체가 콘텐츠의 위치를 나타내는 시스템을 구축할 때 활용될 수 있습니다.
- 폴백(Fallback) 라우트: 특정 경로가 매칭되지 않을 경우, Catch-all 라우트가 해당 요청을 처리하도록 하여 404 에러를 방지하거나, 커스텀 에러 페이지를 보여줄 수 있습니다.
마지막으로 필수 Catch-all과 선택적 Catch-all을 고를 때는 URL 매칭 범위와 params.slug의 형태를 함께 비교해야 합니다.
아래 다이어그램은 두 패턴의 차이를 실무 판단 기준으로 정리합니다.
Catch-all 세그먼트는 여러 단계의 URL을 하나의 파라미터로 처리할 때 사용합니다. URL 패턴이 유동적인 페이지에서 라우트 정의를 단순하게 유지할 수 있습니다.
이것으로 동적 라우트와 Catch-all 세그먼트에 대한 설명을 마칩니다. 다음 절에서는 라우트 그룹을 사용하여 라우트 구조를 논리적으로 조직하는 방법에 대해 알아보겠습니다.
Catch-all 세그먼트 사용에서는 라우팅, 렌더링, 데이터 경계가 실제 서비스 품질에 어떤 순서로 영향을 주는지 점검합니다.
마지막으로 필수 Catch-all과 선택적 Catch-all의 차이를 params 형태와 활용 시나리오로 비교합니다.