Next.js 고급 라우팅 실전: 라우트 그룹, 중첩 레이아웃, 병렬 라우트, 인터셉팅 라우트 완벽 가이드

지난주에 2년째 운영 중인 Next.js 이커머스 프로젝트를 넘겨받았습니다. app 디렉터리를 여는 순간 화면에는 60개가 넘는 폴더가 빽빽하게 들어차 있었습니다. about, products, admin-users, marketing-campaign, shop-cart 등이 모두 한 레벨에 펼쳐져 있었습니다. 사용자 관련 페이지 하나를 찾으려 해도 수많은 마케팅 페이지 사이를 뒤져야 했습니다. 더 답답한 점은 팀원 세 명이 동시에 라우트 파일을 수정하면서 매일 최소 두 번씩 Git 충돌이 났고, 코드 리뷰 때 파일 관계를 파악하는 데만 30분이 걸렸다는 것입니다.
컴퓨터 앞에서 이 폴더들을 바라보다 문득 생각났습니다. Next.js에는 라우트 그룹이나 병렬 라우트 같은 고급 기능이 있지 않았나? 공식 문서를 찾아보니 Next.js 13부터 이미 제공된 기능이었지만 프로젝트에서는 전혀 사용하지 않고 있었습니다. 도구가 부족한 게 아니라 어떤 상황에 무엇을 써야 하는지 몰랐던 셈입니다.
그날 저녁 세 시간 동안 라우팅 기능을 살펴보고, 이어서 이틀 동안 프로젝트 구조를 리팩터링했습니다. 결과는 어땠을까요? 디렉터리 구조가 한눈에 들어오기 시작했고 팀 채팅에서 “또 충돌 났어요”라는 말도 사라졌습니다. 무엇보다 조금 복잡해 보이던 네 가지 기능이 정확히 어떤 문제를 해결하는지 알게 됐습니다. 라우트 그룹은 디렉터리를 질서 있게 정리하고, 중첩 레이아웃은 구조를 유연하게 재사용하며, 병렬 라우트는 여러 페이지를 동시에 표시하고, 인터셉팅 라우트는 모달을 우아하게 구현합니다.
Next.js 프로젝트가 커질수록 파일이 어지러워지고 팀 협업 충돌이 잦아지고 있다면, 이 글을 통해 어떤 라우팅 기능을 언제 어떻게 써야 하는지와 피해야 할 함정을 알 수 있습니다.
라우트 그룹(Route Groups) - 디렉터리를 질서 있게 정리하기
라우트 그룹이란?
결론부터 말하면 라우트 그룹은 (marketing)이나 (shop)처럼 괄호로 감싼 폴더입니다. 흥미로운 점은 Next.js가 이 괄호 안 이름을 URL에 전혀 포함하지 않는다는 것입니다.
별 쓸모가 없어 보이나요? 실제 상황을 만나기 전이라 그렇습니다.
마케팅 페이지(홈페이지, 회사 소개), 쇼핑몰 페이지(상품 목록, 장바구니), 관리자 페이지(주문 관리, 사용자 관리)를 모두 가진 이커머스 사이트가 있다고 해봅시다. 전통적인 방식이라면 페이지를 전부 app 루트에 몰아넣거나 URL에 /marketing, /shop, /admin 접두사를 억지로 붙여야 합니다. 하지만 사용자가 yoursite.com/marketing/about 같은 어색한 주소로 들어오길 원하는 사람은 없을 것입니다.
라우트 그룹은 이 모순을 완벽하게 해결합니다. 파일 시스템에서는 페이지를 기능별로 분류하면서도 사용자에게 보이는 URL에는 영향을 주지 않습니다.
세 가지 핵심 용도(모두 실용적입니다)
용도 1: 팀 또는 기능별로 디렉터리 구분
가장 직접적인 장점입니다. 한 레벨에 펼쳐진 폴더 60개를 세 그룹으로 나눌 수 있습니다.
app/
├── (marketing)/ # 마케팅 팀 담당
│ ├── page.js # 홈페이지 → yoursite.com/
│ ├── about/ # 소개 → yoursite.com/about
│ └── pricing/ # 요금제 → yoursite.com/pricing
├── (shop)/ # 프론트엔드 팀 담당
│ ├── products/ # 상품 → yoursite.com/products
│ └── cart/ # 장바구니 → yoursite.com/cart
└── (dashboard)/ # 백엔드 팀 담당
├── orders/ # 주문 → yoursite.com/orders
└── users/ # 사용자 → yoursite.com/users
URL은 여전히 깔끔한 원래 경로지만 파일 구조는 한눈에 들어옵니다. 새로 온 인턴도 디렉터리만 보고 어느 영역을 담당하는지 알 수 있습니다.
용도 2: 영역마다 서로 다른 루트 레이아웃 사용
라우트 그룹의 가장 강력한 부분입니다. 마케팅 페이지와 관리자 페이지의 내비게이션 바가 같을 수 있을까요? 물론 아닙니다. 하지만 둘 다 루트 경로에서 시작하는 /about과 /orders라면 어떻게 다른 레이아웃을 적용할 수 있을까요?
답은 각 라우트 그룹이 자체 layout.js를 가질 수 있다는 것입니다.
app/
├── (marketing)/
│ ├── layout.js # 마케팅 전용 레이아웃: 상단 탐색 + 히어로 이미지
│ └── ...
├── (shop)/
│ ├── layout.js # 쇼핑몰 전용 레이아웃: 장바구니 아이콘 + 카테고리 필터
│ └── ...
└── (dashboard)/
├── layout.js # 관리자 전용 레이아웃: 사이드바 + 권한 검사
└── ...
세 레이아웃은 서로 간섭하지 않습니다. 마케팅 페이지에는 멋진 히어로 이미지를 넣고, 관리자 페이지에는 sidebar를 배치하며, 쇼핑몰에는 장바구니 수량을 고정 표시할 수 있습니다. 하나의 프로젝트 안에서 모두 구현할 수 있으므로 서브도메인이나 여러 Next.js 인스턴스를 만들 필요가 없습니다.
용도 3: 레이아웃을 선택적으로 공유
때로는 “몇몇 페이지끼리만 레이아웃을 공유하고 나머지는 공유하지 않는” 구성이 필요합니다. 예를 들어 블로그 글에는 모두 왼쪽 목차 탐색이 필요하지만 블로그 홈에는 필요하지 않을 수 있습니다. 라우트 그룹으로 쉽게 구현할 수 있습니다.
app/
├── blog/
│ ├── page.js # 블로그 홈, 왼쪽 탐색 없음
│ └── (articles)/ # 글 그룹, 왼쪽 탐색 공유
│ ├── layout.js # 왼쪽 탐색이 있는 레이아웃
│ ├── [slug]/ # 글 상세 → /blog/xxx
│ └── ...
(articles) 라우트 그룹은 URL에 영향을 주지 않습니다. 접근 경로는 여전히 /blog/my-first-post이지만 이 그룹 안의 페이지만 탐색이 포함된 layout을 사용합니다.
실제 리팩터링 사례: 혼란에서 명료함으로
처음 언급한 폴더 60개의 이커머스 프로젝트로 돌아가 보겠습니다. 리팩터링 전후를 비교하면 다음과 같습니다.
리팩터링 전(일부 파일):
app/
├── page.js
├── about/
├── pricing/
├── products/
├── products-detail/
├── cart/
├── checkout/
├── admin-orders/
├── admin-users/
├── admin-settings/
├── marketing-campaign/
├── ...(50개 더 있음)
파일을 찾으려면 ctrl+F로 하나씩 검색해야 했고, 페이지가 어느 모듈에 속하는지도 파일 이름으로 추측해야 했습니다.
리팩터링 후:
app/
├── (marketing)/
│ ├── layout.js
│ ├── page.js
│ ├── about/
│ ├── pricing/
│ └── campaign/
├── (shop)/
│ ├── layout.js
│ ├── products/
│ ├── cart/
│ └── checkout/
└── (dashboard)/
├── layout.js
├── orders/
├── users/
└── settings/
세 단계 구조가 아주 분명해졌습니다. 마케팅 페이지를 수정하려면 (marketing)으로 가고, 관리자 기능을 추가하려면 (dashboard)에 파일을 추가하면 됩니다. 코드 리뷰 때도 각 팀이 맡은 라우트 그룹만 보면 되므로 충돌률이 바로 70% 줄었습니다.
반드시 피해야 할 세 가지 함정
함정 1: URL 충돌은 곧바로 오류를 일으킵니다
라우트 그룹이 URL에 영향을 주지 않는다면 두 그룹에 같은 이름의 라우트가 있을 때는 어떻게 될까요?
app/
├── (marketing)/
│ └── about/page.js # → /about
└── (shop)/
└── about/page.js # → /about(충돌!)
Next.js는 즉시 Error: Conflicting route 오류를 표시합니다. 해결 방법은 간단합니다. 경로 이름을 바꾸거나 괄호가 없는 실제 디렉터리 계층을 추가하면 됩니다.
app/
├── (marketing)/
│ └── about/page.js # → /about
└── (shop)/
└── shop-info/page.js # → /shop-info(이름 변경)
함정 2: 여러 루트 레이아웃 사이를 이동하면 전체 페이지가 새로 로드됩니다
(shop)에서 (marketing)으로 이동할 때 페이지가 한 번 “깜박이는” 것을 볼 수 있습니다. 버그가 아니라 기능입니다.
서로 다른 라우트 그룹의 루트 레이아웃은 완전히 독립되어 있습니다. 전환할 때 Next.js는 기존 레이아웃을 언마운트하고 새 레이아웃을 마운트해야 하므로 전체 페이지 로드(full page load)가 필요합니다. 두 영역의 레이아웃이 완전히 호환되지 않을 수도 있다는 점을 고려한 의도적인 설계입니다.
부드러운 전환을 원한다면 여러 루트 레이아웃을 사용하지 마세요. 공통 부분을 가장 바깥의 app/layout.js로 올리고 라우트 그룹 안의 layout에는 차이점만 넣으면 됩니다.
함정 3: 여러 루트 레이아웃을 사용할 때의 홈페이지 위치
여러 라우트 그룹을 만들고 각 그룹에 자체 layout.js를 뒀다면 홈페이지 page.js는 반드시 그중 한 그룹 안에 있어야 하며 app/page.js에는 둘 수 없습니다. 그렇지 않으면 Next.js가 어떤 루트 레이아웃을 써야 할지 판단할 수 없습니다.
대부분 홈페이지가 마케팅 콘텐츠에 속하므로 보통 (marketing)/page.js에 둡니다.
라우트 그룹의 핵심은 한 문장으로 정리할 수 있습니다. 파일을 정리하되 URL에는 영향을 주지 않고, 영역마다 서로 다른 레이아웃도 적용할 수 있습니다. 작은 프로젝트에는 필요 없을 수 있지만 app 디렉터리에 이미 폴더가 20개 넘게 있다면 라우트 그룹을 사용해 볼 때입니다.
중첩 레이아웃(Nested Layouts) - 페이지 구조를 유연하게 재사용하기
중첩 레이아웃은 어떤 문제를 해결하나요?
홈페이지에는 상단 탐색이 필요하고, 블로그 목록 페이지에는 상단 탐색과 왼쪽 카테고리 바가 필요하며, 글 상세 페이지에는 여기에 오른쪽 목차 탐색까지 필요했던 경험이 있나요?
기존 컴포넌트 조합 방식이라면 각 페이지에서 이 컴포넌트들을 직접 조립해야 합니다. 내비게이션 바 스타일 하나를 바꾸려 해도 세 곳을 모두 수정해야 합니다.
Next.js의 중첩 레이아웃은 이 문제를 해결합니다. 러시아 인형처럼 레이아웃을 여러 겹 중첩해 바깥 레이아웃을 안쪽 모든 페이지에 자동으로 적용합니다. 한 단계 안으로 들어갈 때마다 바깥 구조를 반복해서 작성하지 않고도 새로운 UI 요소를 추가할 수 있다는 점이 핵심입니다.
중첩 레이아웃은 어떻게 동작하나요?
개념은 간단합니다. 모든 폴더가 자체 layout.js를 가질 수 있고, 하위 폴더는 상위 폴더의 레이아웃을 자동으로 상속한 뒤 그 위에 자체 레이아웃을 한 겹 더 씌웁니다.
온라인 교육 플랫폼을 만든다고 가정해 보겠습니다.
app/
├── layout.js # 루트 레이아웃: 상단 탐색 + Footer
└── courses/
├── layout.js # 강좌 레이아웃: 루트 레이아웃 + 왼쪽 강좌 카테고리
├── page.js # 강좌 목록 페이지
└── [id]/
├── layout.js # 강좌 상세 레이아웃: 강좌 레이아웃 + 오른쪽 진행률 표시줄
└── page.js # 특정 강좌
사용자가 /courses/123에 접근하면 다음 순서로 렌더링됩니다.
- 가장 바깥쪽:
app/layout.js가 전체를 감쌉니다(상단 탐색 + Footer). - 중간 단계:
courses/layout.js가 그 안을 감쌉니다(왼쪽 카테고리 바). - 가장 안쪽:
courses/[id]/layout.js가 한 겹 더 감쌉니다(오른쪽 진행률 표시줄). - 페이지 콘텐츠:
courses/[id]/page.js가 가장 안에 렌더링됩니다.
인형을 겹쳐 넣듯 한 겹씩 중첩됩니다. 상단 탐색을 수정하려면 app/layout.js 하나만 바꾸면 모든 페이지에 자동 반영됩니다.
실전 사례: 블로그 시스템의 3단계 레이아웃
실제 사례로 설명하는 편이 더 명확합니다. 전에 만든 기술 블로그의 요구 사항은 다음과 같았습니다.
- 모든 페이지: 상단 탐색(홈, 소개, 연락처) + 푸터
- 블로그 관련 페이지: 상단 탐색 + 왼쪽 글 카테고리 필터
- 글 상세 페이지: 상단 탐색 + 왼쪽 카테고리 + 오른쪽 목차 앵커
중첩 레이아웃으로 아주 자연스럽게 구현할 수 있습니다.
app/
├── layout.js # 1단계: 사이트 공통 레이아웃
│ └── <Header /><Footer />
└── blog/
├── layout.js # 2단계: 블로그 전용 레이아웃
│ └── <Sidebar />
├── page.js # 블로그 목록 페이지(앞의 두 단계 상속)
└── [slug]/
├── layout.js # 3단계: 글 전용 레이아웃
│ └── <TableOfContents />
└── page.js # 글 상세 페이지(세 단계 모두 상속)
간소화한 코드는 다음과 같습니다.
app/layout.js(1단계)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children} {/* 하위 레이아웃 또는 페이지가 여기에 렌더링됩니다 */}
<Footer />
</body>
</html>
)
}
app/blog/layout.js(2단계)
export default function BlogLayout({ children }) {
return (
<div className="blog-container">
<Sidebar />
<main>{children}</main> {/* 더 안쪽 콘텐츠가 여기에 렌더링됩니다 */}
</div>
)
}
app/blog/[slug]/layout.js(3단계)
export default function ArticleLayout({ children }) {
return (
<div className="article-container">
{children}
<TableOfContents /> {/* 오른쪽 목차 */}
</div>
)
}
각 layout은 자신이 추가할 UI만 신경 쓰면 됩니다. 바깥의 Header나 Sidebar를 알 필요가 없습니다. Next.js가 자동으로 순서에 맞게 중첩합니다.
라우트 그룹과 함께 사용하기
여기서 진짜 힘이 드러납니다. 라우트 그룹은 “가로 방향 분리”(마케팅, 쇼핑몰, 관리자)를 담당하고, 중첩 레이아웃은 “세로 방향 누적”(UI를 한 겹씩 추가)을 담당합니다.
이커머스 프로젝트의 쇼핑몰 영역은 다음과 같이 설계할 수 있습니다.
app/
└── (shop)/
├── layout.js # 쇼핑몰 루트 레이아웃: 장바구니 아이콘 + 상단 카테고리 탐색
├── products/
│ ├── layout.js # 상품 목록 레이아웃: + 왼쪽 필터 바
│ ├── page.js # 목록 페이지
│ └── [id]/
│ ├── layout.js # 상품 상세 레이아웃: + 이동 경로 탐색
│ └── page.js # 상세 페이지
└── cart/
└── page.js # 장바구니 페이지(쇼핑몰 루트 레이아웃만 상속)
- 상품 목록 페이지
/products: 쇼핑몰 루트 레이아웃 + 필터 바 - 상품 상세 페이지
/products/123: 쇼핑몰 루트 레이아웃 + 필터 바 + 이동 경로(세 단계 모두 적용) - 장바구니 페이지
/cart: 더 깊은 layout.js가 없으므로 쇼핑몰 루트 레이아웃만 적용
어떤 페이지에 어떤 UI가 필요한지가 디렉터리 계층으로 완전히 결정됩니다. “상세 페이지라면 이동 경로를 표시한다” 같은 조건문을 잔뜩 쓸 필요가 없습니다.
주의할 두 가지 세부 사항
세부 사항 1: layout은 다시 렌더링되지 않습니다(성능상 유리)
/blog에서 /blog/my-post로 이동할 때 app/layout.js와 app/blog/layout.js는 다시 렌더링되지 않습니다. 상태와 스크롤 위치는 그대로 유지되고 가장 안쪽의 page.js만 다시 로드됩니다.
따라서 layout에 사이드바 스크롤 상태나 검색창 입력값을 둘 수 있고, 사용자가 안쪽 페이지 사이를 이동해도 값이 사라지지 않습니다. 매우 부드러운 경험을 만들 수 있습니다.
세부 사항 2: layout은 하위 라우트의 경로 매개변수에 접근할 수 없습니다
동적 라우트 app/products/[id]/page.js가 있다고 가정합시다. 이 [id] 매개변수는 page.js에서만 params를 통해 얻을 수 있고 layout.js에서는 가져올 수 없습니다.
layout이 [id]에 따라 다른 내용을 렌더링해야 한다면(예: 상품 제목 표시), Context를 사용하거나 데이터를 더 상위 계층으로 올리는 등 다른 방식으로 전달해야 합니다.
중첩 레이아웃의 핵심은 코드 구조를 UI의 시각적 계층과 맞추는 것입니다. 화면의 바깥에서 안쪽으로 구조가 몇 단계인지에 맞춰 폴더에도 layout을 중첩하면 됩니다. 컴포넌트를 여기저기 복사해 붙일 필요 없이 layout 파일 하나를 바꿔 전체 영역의 모든 페이지에 반영할 수 있습니다.
병렬 라우트(Parallel Routes) - 여러 페이지를 동시에 표시하기
어떤 상황에 병렬 라우트가 필요한가요?
실제 상황을 하나 보겠습니다. 관리자 대시보드를 만들면서 다음 세 모듈을 동시에 표시해야 합니다.
- 왼쪽 위: 매출 통계 차트
- 오른쪽 위: 최근 주문 목록
- 아래쪽: 재고 경고
세 모듈은 데이터가 서로 관련 없고 로딩 속도도 다릅니다. 통계 차트 계산은 오래 걸릴 수 있고 주문 목록은 즉시 나오며, 재고 데이터는 별도의 API에서 가져와야 할 수 있습니다.
기존 방식이라면 dashboard/page.js에서 세 API를 한꺼번에 요청하고 세 컴포넌트를 렌더링해야 합니다. 모듈 하나만 오류가 나도 전체 페이지가 망가집니다. 특정 모듈에 독립적인 로딩 상태를 추가하려면 loading state도 직접 많이 작성해야 합니다.
병렬 라우트는 이 문제를 해결합니다. 하나의 페이지에서 여러 개의 독립적인 “페이지 조각”(공식 명칭은 “슬롯” slots)을 동시에 렌더링하며, 각 슬롯은 자체 로딩 상태와 오류 처리, 심지어 독립적인 탐색도 가질 수 있습니다.
병렬 라우트 문법: @ 기호로 슬롯 정의
핵심 문법은 하나뿐입니다. 폴더 이름을 @folder로 지정하면 그 폴더가 “슬롯”이 됩니다.
대시보드 예시는 다음과 같습니다.
app/
└── dashboard/
├── layout.js # 세 슬롯을 props로 받음
├── @sales/ # 슬롯 1: 매출 통계
│ └── page.js
├── @orders/ # 슬롯 2: 주문 목록
│ └── page.js
├── @inventory/ # 슬롯 3: 재고 경고
│ └── page.js
└── page.js # 기본 콘텐츠(선택 사항)
@sales, @orders, @inventory처럼 @로 시작하는 폴더가 병렬 라우트의 슬롯입니다.
그러면 Next.js는 layout.js에 이 슬롯들을 props로 전달합니다.
export default function DashboardLayout({
children, // page.js 콘텐츠에 대응
sales, // @sales/page.js에 대응
orders, // @orders/page.js에 대응
inventory // @inventory/page.js에 대응
}) {
return (
<div className="dashboard">
<div className="widgets">
<div className="widget">{sales}</div>
<div className="widget">{orders}</div>
</div>
<div className="main">{children}</div>
<div className="alerts">{inventory}</div>
</div>
)
}
세 슬롯은 세 개의 독립적인 “하위 페이지”와 같아서 원하는 레이아웃 위치에 자유롭게 배치할 수 있습니다. 모든 로직을 큰 page.js 하나에 밀어 넣을 필요 없이 모듈마다 자체 파일을 둘 수 있습니다.
실전 사례: 관리자 대시보드
세 슬롯의 구체적인 코드는 다음과 같습니다.
@sales/page.js(매출 통계)
async function getSalesData() {
const res = await fetch('https://api.example.com/sales')
return res.json()
}
export default async function SalesWidget() {
const data = await getSalesData() // 이 요청은 느릴 수 있습니다
return (
<div>
<h3>이번 달 매출</h3>
<Chart data={data} />
</div>
)
}
@orders/page.js(주문 목록)
async function getRecentOrders() {
const res = await fetch('https://api.example.com/orders')
return res.json()
}
export default async function OrdersWidget() {
const orders = await getRecentOrders() // 이 요청은 빠를 수 있습니다
return (
<div>
<h3>최근 주문</h3>
<ul>
{orders.map(order => <li key={order.id}>{order.title}</li>)}
</ul>
</div>
)
}
@inventory/page.js(재고 경고)
export default function InventoryWidget() {
// 클라이언트 컴포넌트에서 useEffect로 데이터를 가져와도 됩니다
return (
<div>
<h3>재고 경고</h3>
<p>상품 5개의 재고가 부족합니다</p>
</div>
)
}
여기서 중요한 점은 세 슬롯이 병렬로 로드된다는 것입니다. 주문 목록 API가 빠르면 먼저 렌더링되고, 매출 통계가 느리면 데이터가 도착한 뒤 렌더링됩니다. 슬롯 하나에서 오류가 나더라도 그 슬롯만 실패하고 나머지 둘은 정상적으로 표시됩니다.
기존 방식보다 훨씬 깔끔합니다. 세 개의 loading state를 직접 관리할 필요가 없고, 가장 느린 API를 기다릴 필요도 없으며, 모듈마다 자연스럽게 격리됩니다.
독립적인 loading 및 error 상태
각 슬롯에는 자체 loading.js와 error.js도 둘 수 있습니다.
app/
└── dashboard/
├── @sales/
│ ├── page.js
│ ├── loading.js # 매출 모듈의 로딩 상태
│ └── error.js # 매출 모듈의 오류 처리
├── @orders/
│ ├── page.js
│ └── loading.js # 주문 모듈의 로딩 상태
└── @inventory/
└── page.js
@sales/loading.js:
export default function SalesLoading() {
return <div>매출 데이터를 불러오는 중...</div>
}
@sales/error.js:
'use client'
export default function SalesError({ error, reset }) {
return (
<div>
<p>매출 데이터를 불러오지 못했습니다</p>
<button onClick={reset}>다시 시도</button>
</div>
)
}
이제 페이지를 열면 매출 모듈에는 “불러오는 중…”이 표시되고, 주문 모듈은 이미 렌더링됐을 수 있으며, 재고 모듈은 즉시 표시됩니다. 매출 API에 문제가 생겨도 해당 모듈에만 “불러오지 못했습니다”가 표시되고 다른 모듈은 전혀 영향을 받지 않습니다.
모든 데이터가 올 때까지 빈 화면을 바라볼 필요 없이 준비된 콘텐츠부터 볼 수 있어 사용자 경험이 크게 좋아집니다.
핵심: default.js의 역할
/dashboard 페이지에서 링크를 눌러 /dashboard/settings로 이동했다고 가정해 봅시다. @sales, @orders 슬롯의 라우트는 /dashboard/settings와 일치하지 않습니다. 이때 Next.js는 무엇을 렌더링해야 할까요?
기본 동작은 슬롯의 기존 콘텐츠를 그대로 유지하는 것입니다(SPA의 부분 업데이트와 비슷합니다). 하지만 라우트 전환 시 슬롯을 숨기고 싶다면 default.js가 필요합니다.
app/
└── dashboard/
├── @sales/
│ ├── page.js
│ └── default.js # 일치하지 않을 때 null 반환
└── ...
@sales/default.js:
export default function SalesDefault() {
return null // 일치하지 않을 때 아무것도 표시하지 않음
}
이제 /dashboard/settings로 이동하면 @sales 슬롯은 default.js의 콘텐츠, 즉 아무것도 렌더링하지 않습니다.
병렬 라우트가 가장 많이 쓰이는 경우는 사실 인터셉팅 라우트와 결합해 모달을 구현할 때입니다. 다음 장에서 자세히 다루겠습니다. 물론 여러 독립 모듈을 조합하는 대시보드에도 병렬 라우트는 매우 유용합니다.
인터셉팅 라우트(Intercepting Routes) - 모달을 우아하게 구현하기
Instagram의 사진 보기 경험
Instagram이나 Xiaohongshu를 사용해 본 적이 있을 것입니다. Feed에서 사진을 누르면 모달 형태로 열리고 브라우저 주소 표시줄은 /photo/abc123으로 바뀝니다. 흥미로운 점은 다음과 같습니다.
- 브라우저의 뒤로 버튼을 누르면 전혀 다른 이전 페이지로 이동하는 대신 모달이 닫히고 Feed로 돌아갑니다.
- 페이지를 새로고침하면 모달이 사라지고 사진 상세 전체 페이지가 표시됩니다.
- URL을 다른 사람에게 공유하면 모달이 아니라 전체 페이지가 열립니다.
기존 방식으로 구현하려면 상태 관리, URL 파싱, history 조작 코드를 많이 작성해야 해서 복잡합니다.
인터셉팅 라우트(Intercepting Routes)는 이 상황을 위해 설계됐습니다. 클라이언트 탐색을 가로채 현재 페이지에서 대상 라우트 콘텐츠를 모달로 보여 주지만, URL을 직접 열거나 새로고침하면 전체 페이지를 렌더링합니다.
인터셉팅 라우트 문법: (..) 기호
핵심 문법은 괄호와 점을 사용해 “어느 단계의 라우트를 인터셉트할지” 표현하는 것입니다.
(.)— 동일 레벨 라우트 인터셉트(..)— 한 단계 상위 라우트 인터셉트(..)(..)— 두 단계 상위 라우트 인터셉트(...)— 루트 디렉터리부터 라우트 인터셉트
추상적으로 들릴 수 있지만 예시를 보면 바로 이해할 수 있습니다.
사진 목록 페이지 /photos에서 사진을 눌러 /photos/123으로 이동한다고 가정해 봅시다. 원하는 동작은 다음과 같습니다.
- 클릭해서 이동할 때: 목록 페이지 위에 모달 표시
- 직접 접근하거나 새로고침할 때: 사진 상세 전체 페이지 표시
디렉터리 구조는 다음과 같습니다.
app/
├── @modal/
│ ├── (.)photos/ # 동일 레벨의 photos 라우트 인터셉트
│ │ └── [id]/
│ │ └── page.js # 모달 콘텐츠
│ └── default.js # 일치하지 않을 때 null 반환
├── layout.js # modal 슬롯을 받음
├── page.js # 홈페이지 Feed
└── photos/
└── [id]/
└── page.js # 사진 상세 전체 페이지
@modal/(.)photos/는 “@modal과 동일한 레벨에 있는 photos 라우트를 인터셉트한다”는 뜻입니다. @modal과 photos가 모두 app/ 아래 있으므로 (.)를 사용합니다.
실전 사례: Instagram 스타일 사진 모달
전체 과정을 구현해 보겠습니다. 요구 사항은 명확합니다.
- 홈페이지에 사진 그리드 표시
- 사진을 클릭하면 모달이 열리고 URL은
/photos/123으로 변경 - 새로고침하거나
/photos/123에 직접 접근하면 전체 페이지 표시 - 뒤로 버튼을 누르면 모달 닫기
1단계: 디렉터리 구조
app/
├── @modal/
│ ├── (.)photos/
│ │ └── [id]/
│ │ └── page.js # 모달 컴포넌트
│ └── default.js
├── layout.js
├── page.js # 홈페이지 사진 그리드
└── photos/
└── [id]/
└── page.js # 사진 전체 페이지
2단계: 루트 레이아웃에서 modal 슬롯 받기
app/layout.js:
export default function RootLayout({ children, modal }) {
return (
<html>
<body>
{children} {/* 기본 콘텐츠 영역 */}
{modal} {/* 모달 슬롯 */}
</body>
</html>
)
}
3단계: 홈페이지에 사진 그리드 표시
app/page.js:
import Link from 'next/link'
const photos = [
{ id: '1', url: '/images/photo1.jpg' },
{ id: '2', url: '/images/photo2.jpg' },
// ...
]
export default function HomePage() {
return (
<div className="photo-grid">
{photos.map(photo => (
<Link key={photo.id} href={`/photos/${photo.id}`}>
<img src={photo.url} alt="" />
</Link>
))}
</div>
)
}
4단계: 인터셉팅 라우트 - 모달 컴포넌트
app/@modal/(.)photos/[id]/page.js:
'use client'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
export default function PhotoModal({ params }) {
const router = useRouter()
return (
<div className="modal-backdrop" onClick={() => router.back()}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button onClick={() => router.back()}>닫기</button>
<Image src={`/images/photo${params.id}.jpg`} fill />
</div>
</div>
)
}
여기서는 router.back()으로 모달을 닫습니다. 클라이언트 탐색으로 들어왔기 때문에 뒤로 이동하면 Feed로 돌아갈 수 있습니다.
5단계: 전체 페이지
app/photos/[id]/page.js:
import Image from 'next/image'
export default function PhotoPage({ params }) {
return (
<div className="photo-page">
<nav>홈으로 돌아가기</nav>
<h1>사진 상세 정보</h1>
<Image src={`/images/photo${params.id}.jpg`} width={800} height={600} />
<p>사진 설명...</p>
</div>
)
}
6단계: default.js로 모달이 닫히도록 보장
app/@modal/default.js:
export default function Default() {
return null // 라우트가 일치하지 않을 때 모달을 표시하지 않음
}
마법 같은 동작: 실제 경험
이제 다음과 같이 동작합니다.
-
홈페이지에서 사진 클릭:
- Link 컴포넌트가
/photos/1로 클라이언트 탐색을 실행합니다. - Next.js가
@modal/(.)photos/[id]의 일치를 감지하고 탐색을 인터셉트합니다. - 모달 컴포넌트를 렌더링해 Feed 위에 겹쳐 표시합니다.
- URL은
/photos/1로 바뀌지만 페이지 전체가 새로고침되지는 않습니다.
- Link 컴포넌트가
-
뒤로 버튼 누르기:
router.back()이 이전 라우트(홈페이지/)로 돌아갑니다.@modal슬롯이 더 이상 일치하지 않아default.js(null)를 렌더링합니다.- 모달이 사라지고 Feed는 원래 상태를 유지합니다.
-
페이지를 새로고침하거나
/photos/1에 직접 접근:- 클라이언트 탐색이 아니므로 Next.js가 인터셉트하지 않습니다.
app/photos/[id]/page.js의 전체 페이지를 직접 렌더링합니다.- 모달 없이 독립된 사진 상세 페이지가 표시됩니다.
-
링크 공유:
- 다른 사람이
/photos/1을 열면 전체 페이지를 보게 됩니다. - 직접 접근했을 때와 같은 경험입니다.
- 다른 사람이
공유 가능한 URL, 뒤로 버튼을 통한 모달 닫기, 새로고침 시 전체 페이지 표시라는 세 요구 사항을 모두 충족합니다.
인터셉트 단계는 어떻게 선택하나요?
앞서 설명한 (.), (..), (...) 중 무엇을 써야 할지는 인터셉트 위치와 대상 라우트의 상대 위치에 따라 결정됩니다.
인터셉팅 라우트가 app/@modal/ 아래 있다고 가정하면 다음과 같습니다.
- 대상 라우트가
app/photos/(동일 레벨) →(.)photos사용 - 대상 라우트가
app/shop/products/(상위 단계의 하위 라우트) →(..)사용 - 대상 라우트가 임의의 위치(예: 깊은 중첩) →
(...)로 루트부터 인터셉트
예를 들어 디렉터리 구조가 다음과 같다면:
app/
└── shop/
├── @modal/
│ └── (..)products/ # 상위 단계의 products 인터셉트
│ └── [id]/
└── products/
└── [id]/
여기서 @modal은 shop/ 아래 있고 shop/products/를 인터셉트해야 하므로 products가 위치한 상위 단계인 shop을 가리키기 위해 (..)를 사용합니다.
처음에는 조금 헷갈릴 수 있습니다. 먼저 (...)로 루트부터 인터셉트해 정상 동작하게 만든 뒤 실제 디렉터리 구조에 따라 (.) 또는 (..)로 조정하는 방법을 권합니다.
주의해야 할 세 가지 함정
함정 1: default.js를 잊어서 모달이 닫히지 않음
@modal에 default.js가 없으면 라우트가 일치하지 않을 때도 슬롯이 이전 콘텐츠를 유지해 모달이 계속 남아 있을 수 있습니다. 반드시 null을 반환하는 default.js를 추가해야 합니다.
함정 2: Server Component에서 useRouter 사용
인터셉팅 라우트의 모달은 보통 useRouter().back()으로 닫기 때문에 Client Component여야 합니다. 'use client' 지시문을 잊지 마세요.
함정 3: 여러 단계로 중첩됐을 때 인터셉트 실패
디렉터리 구조가 깊다면(예: app/shop/(store)/products/[id]) 인터셉트 경로의 단계를 정확히 계산해야 합니다. 잘 모르겠다면 (...)로 루트부터 인터셉트하세요. 덜 세련돼 보일 수 있지만 잘못될 가능성은 낮습니다.
인터셉팅 라우트와 병렬 라우트의 조합은 모달의 모든 어려움을 해결합니다. URL 공유, 새로고침 시 전체 페이지 표시, 뒤로 이동 시 모달 닫기, 앞으로 이동 시 다시 열기가 가능합니다. Instagram, Twitter, Airbnb에서 볼 수 있는 경험을 Next.js에서도 구현할 수 있습니다.
종합 실전 - 네 가지 기술 함께 사용하기
실제 프로젝트 상황: 이커머스 플랫폼의 전체 구조
앞의 네 장에서 네 가지 라우팅 기능을 각각 설명했습니다. 따로 보면 “쓸 만하다” 정도로 느낄 수 있지만 진정한 힘은 이 기능들을 조합할 때 나타납니다. 실제 이커머스 플랫폼 요구 사항으로 라우트 그룹, 중첩 레이아웃, 병렬 라우트, 인터셉팅 라우트를 모두 사용하는 방법을 살펴보겠습니다.
요구 사항 분석
중간 규모의 이커머스 플랫폼을 만든다고 가정해 봅시다. 요구 사항은 다음과 같습니다.
세 가지 주요 기능 영역(서로 간섭하지 않는 레이아웃):
- 마케팅 영역(
/,/about,/pricing): 큰 배경 이미지 + 간결한 탐색 - 쇼핑몰 영역(
/products,/cart): 고정 장바구니 아이콘 + 상품 카테고리 탐색 - 관리자 영역(
/dashboard): 사이드바 + 권한 검사
쇼핑몰 영역의 세부 요구 사항:
- 상품 목록 페이지에 왼쪽 필터 바 필요
- 상품 상세 페이지에 이동 경로 탐색 필요
- 상품 카드를 클릭하면 목록 페이지를 떠나지 않고 빠른 미리보기 모달 표시
- 새로고침하거나 상세 URL에 직접 접근하면 전체 상세 페이지 표시
관리자 대시보드 요구 사항:
- 매출 통계, 주문 목록, 재고 경고라는 세 독립 모듈을 동시에 표시
- 각 모듈에 자체 로딩 상태와 오류 처리 제공
전체 디렉터리 구조 설계
코드보다 먼저 전체 구조를 살펴보겠습니다. 각 단계에서 어떤 기술을 사용하는지 확인하세요.
app/
├── layout.js # 전역 루트 레이아웃
│
├── (marketing)/ # 라우트 그룹: 마케팅 영역
│ ├── layout.js # 마케팅 전용 레이아웃
│ ├── page.js # 홈페이지 → /
│ ├── about/ # 소개 → /about
│ └── pricing/ # 요금제 → /pricing
│
├── (shop)/ # 라우트 그룹: 쇼핑몰 영역
│ ├── layout.js # 쇼핑몰 전용 레이아웃
│ ├── @modal/ # 병렬 라우트: 모달 슬롯
│ │ ├── (.)products/ # 인터셉팅 라우트: 상품 빠른 미리보기
│ │ │ └── [id]/
│ │ │ └── page.js # 모달 컴포넌트
│ │ └── default.js
│ │
│ ├── products/
│ │ ├── layout.js # 중첩 레이아웃: 상품 필터 바
│ │ ├── page.js # 목록 페이지 → /products
│ │ └── [id]/
│ │ ├── layout.js # 중첩 레이아웃: 이동 경로
│ │ └── page.js # 상세 페이지 → /products/123
│ │
│ └── cart/
│ └── page.js # 장바구니 → /cart
│
└── (dashboard)/ # 라우트 그룹: 관리자
├── layout.js # 관리자 전용 레이아웃(사이드바)
├── @sales/ # 병렬 라우트: 매출 통계
│ ├── page.js
│ └── loading.js
├── @orders/ # 병렬 라우트: 주문 목록
│ ├── page.js
│ └── loading.js
├── @inventory/ # 병렬 라우트: 재고 경고
│ └── page.js
└── page.js # 대시보드 홈 → /dashboard
이 구조에서는 다음과 같이 역할이 나뉩니다.
- 라우트 그룹이 세 영역을 격리합니다.
- 중첩 레이아웃이 쇼핑몰 영역에 UI를 단계별로 추가합니다.
- 병렬 라우트가 쇼핑몰에는 모달 슬롯을, 관리자에는 다중 모듈 지원을 제공합니다.
- 인터셉팅 라우트가 상품 빠른 미리보기를 구현합니다.
핵심 코드 구현
전체 코드는 너무 길기 때문에 몇 가지 핵심만 살펴보겠습니다.
1. 쇼핑몰 영역 레이아웃에서 모달 슬롯 받기
app/(shop)/layout.js:
export default function ShopLayout({ children, modal }) {
return (
<div>
<nav>{/* 장바구니 아이콘 + 카테고리 탐색 */}</nav>
{children}
{modal} {/* 모달이 여기에 겹쳐 표시됩니다 */}
</div>
)
}
2. 상품 목록 페이지의 중첩 레이아웃
app/(shop)/products/layout.js:
export default function ProductsLayout({ children }) {
return (
<div className="products-container">
<aside>{/* 왼쪽 필터 바 */}</aside>
<main>{children}</main>
</div>
)
}
app/(shop)/products/page.js(목록 페이지):
import Link from 'next/link'
export default function ProductsPage() {
return (
<div className="product-grid">
{products.map(p => (
<Link key={p.id} href={`/products/${p.id}`}>
<ProductCard product={p} />
</Link>
))}
</div>
)
}
상품 카드를 클릭하면 클라이언트 탐색이 실행되고, 인터셉팅 라우트가 동작해 모달이 열립니다.
3. 인터셉팅 라우트로 빠른 미리보기 구현
app/(shop)/@modal/(.)products/[id]/page.js:
'use client'
import { useRouter } from 'next/navigation'
export default function ProductModal({ params }) {
const router = useRouter()
return (
<div className="modal-backdrop" onClick={() => router.back()}>
<div className="modal">
<h2>상품 빠른 미리보기</h2>
<ProductPreview id={params.id} />
<Link href={`/products/${params.id}`} onClick={() => router.back()}>
전체 상세 정보 보기
</Link>
</div>
</div>
)
}
4. 관리자 대시보드의 병렬 라우트
app/(dashboard)/layout.js:
export default function DashboardLayout({ children, sales, orders, inventory }) {
return (
<div className="dashboard">
<aside>{/* 사이드바 탐색 */}</aside>
<main>
{children}
<div className="widgets">
<div className="widget">{sales}</div>
<div className="widget">{orders}</div>
<div className="widget">{inventory}</div>
</div>
</main>
</div>
)
}
세 슬롯은 병렬로 로드됩니다. 빠른 것은 먼저 나오고 느린 것은 기다렸다 나오며 서로 영향을 주지 않습니다.
설계 결정의 이유
왜 이렇게 설계했을까요? 모든 결정에는 이유가 있습니다.
라우트 그룹을 사용하는 이유는 무엇인가요?
- 마케팅, 쇼핑몰, 관리자의 내비게이션 바가 완전히 다르므로 서로 다른 루트 레이아웃이 필요합니다.
- 협업할 때 세 팀이 각자의 라우트 그룹을 담당하므로 충돌이 줄어듭니다.
중첩 레이아웃을 사용하는 이유는 무엇인가요?
- 상품 목록에는 필터 바가, 상세 페이지에는 이동 경로가 필요하지만 둘 다 쇼핑몰의 상단 탐색을 상속해야 합니다.
- 중첩 레이아웃을 사용하면 UI 계층과 디렉터리 계층이 대응되어 코드가 명확해집니다.
모달에 인터셉팅 라우트 + 병렬 라우트를 사용하는 이유는 무엇인가요?
- 사용자가 현재 목록 페이지를 떠나지 않고 상품을 빠르게 미리 볼 수 있어야 합니다.
- 공유와 SEO를 위해 URL은
/products/123으로 바뀌어야 합니다. - 새로고침하면 모달만 보이는 것이 아니라 전체 페이지가 표시돼야 합니다.
관리자에서 병렬 라우트를 사용하는 이유는 무엇인가요?
- 세 모듈의 데이터 출처와 로딩 속도가 서로 다릅니다.
- 병렬 라우트는 각 모듈을 독립적으로 로드하고 독립적으로 오류를 처리하게 합니다.
- 모듈 하나가 실패해도 다른 모듈에는 영향을 주지 않습니다.
팀 협업에서 얻은 실제 효과
이 구조로 리팩터링한 뒤 팀에서 확인한 실제 변화는 다음과 같습니다.
- 충돌률 65% 감소: 프론트엔드 팀은
(shop)을, 백엔드 팀은(dashboard)를 수정하므로 서로 영향을 주지 않습니다. - 적응 시간 절반으로 단축: 새 팀원이 디렉터리 트리만 봐도 어떤 파일이 무엇을 담당하는지 알 수 있어 코드를 뒤져 추측할 필요가 없습니다.
- 유지보수 비용 감소: 내비게이션 바를 수정할 때 해당 라우트 그룹의
layout.js만 바꾸면 되므로 다른 영역을 실수로 건드리지 않습니다. - 사용자 경험 향상: 상품 빠른 미리보기의 전환율이 기존 상세 페이지 이동 방식보다 23% 높아졌습니다(사용자가 뒤로 가기 버튼을 누르는 것을 번거롭게 느끼기 때문입니다).
결국 이 네 가지 라우팅 기능은 “기술을 과시하기 위한 것”이 아니라 코드 구조를 더 명확하게 만들고, 팀 협업을 원활하게 하며, 사용자 경험을 부드럽게 만드는 것입니다. 작은 프로젝트에는 필요 없지만 라우트가 수십 개이고 여러 팀이 협업하며 복잡한 모달 상호작용까지 필요하다면 이 구성이 큰 도움이 됩니다.
결론
처음의 폴더 60개가 뒤엉킨 프로젝트로 돌아가 보겠습니다. 리팩터링을 마친 뒤 가장 크게 느낀 점은 기술이 대단하다는 것이 아니라 드디어 비즈니스 로직 작성에 집중하고 파일 찾기, 충돌 해결, 레이아웃 유지보수에 시간을 낭비하지 않아도 된다는 것이었습니다.
네 가지 라우팅 기능을 빠르게 정리하면 다음과 같습니다.
| 기능 | 핵심 역할 | 적합한 상황 | 핵심 문법 |
|---|---|---|---|
| 라우트 그룹 | 파일 정리, 레이아웃 격리 | 여러 기능 영역, 팀 협업 | (folderName) |
| 중첩 레이아웃 | UI를 단계별로 누적 | 다단계 탐색, 요소의 단계적 추가 | 각 단계의 layout.js |
| 병렬 라우트 | 여러 페이지 조각을 동시에 렌더링 | 대시보드, 독립 모듈 | @folderName |
| 인터셉팅 라우트 | 탐색을 인터셉트해 모달 표시 | Instagram 스타일 모달 | (.) (..) (...) |
처음부터 전부 사용하지 않는 것을 권합니다. 먼저 라우트 그룹으로 복잡한 디렉터리를 정리하세요. “다단계 UI 누적”이 필요할 때 중첩 레이아웃을 사용하고, 대시보드나 모달을 만들 때 병렬 라우트와 인터셉팅 라우트를 고려하면 됩니다.
마지막으로 한 가지 경험을 덧붙이겠습니다. 이런 고급 기능은 처음에는 분명 조금 헷갈립니다. 저도 공식 문서를 처음 봤을 때 한동안 이해하지 못했습니다. 하지만 한 번 사용하고 나면 설계 논리가 매우 자연스럽다는 것을 알게 됩니다. 디렉터리 구조는 곧 라우트이고, 파일 계층은 곧 UI 계층이며, 인터셉트 로직은 곧 사용자 경험입니다.
Next.js 프로젝트가 비대하고 복잡해지기 시작했다면 반나절 정도 시간을 내 라우트 그룹으로 리팩터링해 보세요. 석 달 뒤의 자신이 오늘의 결정을 고마워할 것입니다.
Next.js 고급 라우팅 리팩터링 전체 과정
복잡한 디렉터리 구조를 명확한 라우트 그룹, 중첩 레이아웃, 병렬 라우트, 인터셉팅 라우트 아키텍처로 리팩터링합니다.
⏱️ Estimated time: 4 hr
- 1
Step 1: 기존 프로젝트 구조 분석
현재 프로젝트의 라우트 수와 복잡도를 평가합니다.
• app 디렉터리 아래 폴더 수를 집계합니다(20개를 넘으면 라우트 그룹 사용 권장).
• 마케팅, 쇼핑몰, 관리자 등 서로 다른 기능 영역을 식별합니다.
• 서로 다른 레이아웃이 필요한 페이지 그룹을 찾습니다.
• 팀 협업에서 충돌이 발생하는 지점을 기록합니다.
판단 기준:
• 폴더 수 > 20: 라우트 그룹 사용
• 다단계 탐색 필요: 중첩 레이아웃 사용
• 여러 독립 모듈을 동시에 표시해야 함: 병렬 라우트 사용
• 모달 상호작용 필요: 인터셉팅 라우트 + 병렬 라우트 사용 - 2
Step 2: 라우트 그룹으로 기능 영역 구분
괄호를 사용해 기능 또는 팀 단위로 라우트 그룹을 만듭니다.
• (marketing) 그룹 생성: 홈페이지, 소개, 요금제 등 마케팅 관련 페이지 배치
• (shop) 그룹 생성: 상품, 장바구니 등 쇼핑몰 관련 페이지 배치
• (dashboard) 그룹 생성: 주문, 사용자 등 관리자 페이지 배치
주의 사항:
• 라우트 그룹 이름은 URL에 영향을 주지 않지만 동일한 URL이 여러 그룹에 존재할 수는 없습니다.
• 각 라우트 그룹은 자체 layout.js를 가질 수 있습니다.
• 홈페이지 page.js는 반드시 어느 한 라우트 그룹 안에 있어야 합니다(app/page.js에 둘 수 없음). - 3
Step 3: 중첩 레이아웃 계층 설계
UI 계층에 맞춰 중첩 레이아웃을 설계합니다.
• 1단계: app/layout.js(사이트 공통 레이아웃: Header + Footer)
• 2단계: 기능 영역 layout.js(예: blog/layout.js에서 사이드바 추가)
• 3단계: 상세 페이지 layout.js(예: blog/[slug]/layout.js에서 목차 탐색 추가)
구현 핵심:
• 각 layout에는 해당 단계에만 필요한 UI 요소를 추가합니다.
• 하위 레이아웃은 상위 레이아웃을 자동으로 상속합니다.
• layout은 다시 렌더링되지 않아 성능이 좋습니다. - 4
Step 4: 병렬 라우트 구현(필요한 경우)
@ 기호로 슬롯을 만듭니다.
• @modal 슬롯 생성: 모달에 사용
• @sales, @orders 슬롯 생성: 대시보드의 독립 모듈에 사용
layout.js에서 슬롯을 받습니다.
• export default function Layout({ children, modal, sales, orders })
• JSX에서 렌더링: {modal} {sales} {orders}
각 슬롯은 자체 loading.js와 error.js를 가질 수 있습니다. - 5
Step 5: 인터셉팅 라우트 구현(모달이 필요한 경우)
인터셉팅 라우트 구조를 만듭니다.
• @modal 아래에 (.)photos/[id]/page.js 생성(동일 레벨 라우트 인터셉트)
• photos/[id]/page.js 생성(전체 페이지)
문법 설명:
• (.): 동일 레벨 라우트 인터셉트
• (..): 한 단계 상위 라우트 인터셉트
• (...): 루트 디렉터리부터 인터셉트
모달이 닫히도록 null을 반환하는 default.js를 반드시 만듭니다. - 6
Step 6: 테스트 및 검증
모든 기능을 검증합니다.
• 라우트 그룹 테스트: URL은 바뀌지 않고 파일 구조만 명확해졌는지 확인
• 중첩 레이아웃 테스트: UI 계층과 상태 유지가 올바른지 확인
• 병렬 라우트 테스트: 모듈이 독립적으로 로드되고 오류가 격리되는지 확인
• 인터셉팅 라우트 테스트: 클라이언트 탐색에서는 모달이 표시되고 새로고침 시에는 전체 페이지가 표시되는지 확인
성능 점검:
• Next.js DevTools로 레이아웃 렌더링 횟수 확인
• 하위 라우트 전환 시 layout이 다시 렌더링되지 않는지 확인
FAQ
라우트 그룹이 URL에 영향을 주나요?
언제 라우트 그룹을 사용해야 하나요?
중첩 레이아웃이 성능에 영향을 주나요?
병렬 라우트와 일반 컴포넌트의 차이는 무엇인가요?
인터셉팅 라우트 문법 (.), (..), (...) 중 무엇을 선택해야 하나요?
인터셉팅 라우트에 default.js가 필요한 이유는 무엇인가요?
Pages Router에서 App Router로 마이그레이션할 때 라우트를 리팩터링해야 하나요?
6분 읽기 · 게시일: 2025년 12월 18일 · 수정일: 2026년 9월 4일
Next.js 완전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Next.js Pages Router에서 App Router로 마이그레이션하는 실전 가이드: 점진적 전략과 함정 체크리스트
Next.js Pages Router에서 App Router로 처음부터 끝까지 마이그레이션하는 방법을 소개합니다. 점진적 마이그레이션 방안, 자주 만나는 7가지 함정의 해결법, 실제 프로덕션 프로젝트 경험을 바탕으로 Next.js 14로 안전하게 업그레이드할 수 있도록 돕습니다.
45편 중 3편
다음
Next.js 동적 라우팅과 매개변수 처리 완벽 가이드: 기초부터 타입 안전성까지
Next.js 14+의 동적 라우팅 시스템을 단계별로 익혀 봅니다. 동적 매개변수, catch-all 라우트, 선택적 매개변수, generateStaticParams를 사용해야 하는 시점, TypeScript 타입 안전성 실전까지 다룹니다. 라우트 매개변수 조회 방식의 변화로 생기는 혼란을 해결할 수 있도록 다양한 실전 코드 예제를 함께 제공합니다.
45편 중 5편



댓글
GitHub로 로그인하여 댓글을 남기세요