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의 가변적인 부분을 유연하게 처리할 수 있게 해줍니다.
필수 Catch-all 세그먼트 구현하기
필수 Catch-all 세그먼트 [...slug]
는 블로그에서 카테고리와 게시물 제목 외에, 임의의 깊이를 가진 서브 카테고리를 처리하고 싶을 때 유용합니다. 예를 들어 /articles/programming/javascript/nextjs-deep-dive
와 같은 경로를 처리할 수 있습니다.
예시: 문서 사이트 구현
우리는 /docs/path/to/document
와 같은 경로를 처리하는 문서를 만들 것입니다.
-
src/app/docs
폴더 생성: 먼저src/app
안에docs
폴더를 생성합니다.src/ └── app/ ├── docs/ <- 여기에 docs 폴더 생성 └── ...
-
[...slug]
폴더 생성:src/app/docs
안에[...slug]
라는 이름의 폴더를 생성합니다.src/ └── app/ ├── docs/ │ └── [...slug]/ <- 여기에 [...slug] 폴더 생성 └── ...
-
page.tsx
파일 생성:src/app/docs/[...slug]
폴더 안에page.tsx
파일을 생성합니다.src/ └── app/ ├── docs/ │ └── [...slug]/ │ └── page.tsx <- 여기에 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-document
http://localhost:3000/docs/category/sub-category/article-title
http://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
-
src/app/docs/[[...slug]]/page.tsx
파일 내용 수정:page.tsx
파일 내용은 거의 동일하지만,params.slug
가undefined
일 수 있다는 점을 고려하여 코드를 수정합니다.// src/app/docs/[[...slug]]/page.tsx 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-document
http://localhost:3000/docs/category/sub-category/article-title
/docs
경로가 매칭되어 문서 홈 페이지가 표시되고, 다른 경로들도 이전처럼 잘 동작하는 것을 확인할 수 있습니다.
Catch-all 세그먼트의 활용 시나리오
- 동적인 URL 구조 처리: 문서, 블로그, 위키 등 계층적이고 유연한 URL 구조가 필요한 경우에 유용합니다.
- 파일 시스템 기반 CMS: 파일 경로 자체가 콘텐츠의 위치를 나타내는 시스템을 구축할 때 활용될 수 있습니다.
- 폴백(Fallback) 라우트: 특정 경로가 매칭되지 않을 경우, Catch-all 라우트가 해당 요청을 처리하도록 하여 404 에러를 방지하거나, 커스텀 에러 페이지를 보여줄 수 있습니다.
Catch-all 세그먼트는 Next.js App Router의 유연성을 극대화하는 강력한 도구입니다. 이를 통해 어떤 URL 패턴도 효율적으로 처리하고, 더욱 견고하고 사용자 친화적인 애플리케이션을 구축할 수 있습니다.
이것으로 동적 라우트와 Catch-all 세그먼트에 대한 설명을 마칩니다. 다음 절에서는 라우트 그룹을 사용하여 라우트 구조를 논리적으로 조직하는 방법에 대해 알아보겠습니다.