Next.js 병렬 라우트와 인터셉트 라우트: 모달 패턴과 default.js
사진 목록에서 사진을 누르면 모달로 크게 보여 주고, 같은 주소를 새 탭에서 열면 전체 페이지로 보여 주는 화면이 있습니다. 주소는 /photo/123 하나인데 보이는 모양이 둘입니다.
모달 상태를 useState 로 들고 있으면 이 요구를 맞추기 어렵습니다. 모달을 연 상태에서 주소가 바뀌지 않으니 공유할 수 없고, 반대로 주소를 바꾸면 목록이 사라집니다. Next.js 는 이 조합을 위해 라우팅 규칙 두 가지를 제공합니다.
한 화면에 한 페이지만 그린다는 생각
앱 라우터를 배우면 「주소 하나에 페이지 하나」라는 모델이 먼저 자리 잡습니다. 폴더가 주소가 되고, 그 폴더의 page.tsx 가 화면이 됩니다.
이 모델로는 설명되지 않는 화면이 있습니다. 대시보드처럼 한 화면에 독립적인 영역이 여럿 있고 각 영역이 자기 하위 페이지를 따로 갖는 경우입니다. 그리고 앞의 사진 모달처럼 같은 주소가 진입 경로에 따라 다르게 보여야 하는 경우입니다.
병렬 라우트: 한 레이아웃에 여러 페이지
병렬 라우트 문서는 이 기능을 「같은 뷰에서 하나 이상의 페이지를 동시에 렌더링하고 각각 독립적으로 이동할 수 있게 하는 것」으로 설명합니다.
만드는 방법은 @폴더 규칙입니다. 이것을 슬롯이라고 부릅니다.
app/
├── layout.tsx
├── page.tsx
├── @analytics/
│ └── page.tsx
└── @team/
└── page.tsx레이아웃은 슬롯을 props 로 받습니다.
export default function Layout({ children, analytics, team }) {
return (
<>
{children}
{analytics}
{team}
</>
);
}중요한 규칙이 둘 있습니다. 첫째, 슬롯은 주소에 나타나지 않습니다. 문서는 /@analytics/views 의 URL 이 /views 라고 적습니다. 둘째, children 도 이름이 없는 슬롯입니다. app/page.js 는 app/@children/page.js 와 같습니다.
새로고침하면 사라지는 슬롯과 default.js
병렬 라우트에서 가장 자주 만나는 함정이 이것입니다. 문서의 설명입니다.
@team 에는 /settings 페이지가 있고 @analytics 에는 없다고 해 보겠습니다. /settings 로 클라이언트 이동을 하면 @team 은 설정 화면으로 바뀌고 @analytics 는 보고 있던 화면을 유지합니다. 그런데 그 상태에서 새로고침하면 Next.js 는 @analytics 의 default.js 를 렌더링합니다. 그 파일이 없으면 404 입니다.
children 도 암묵적 슬롯이므로, 부모 페이지의 활성 상태를 복구할 수 없을 때를 대비해 default.js 가 필요할 수 있다고 문서는 덧붙입니다.
정리하면 병렬 라우트를 쓸 때는 슬롯마다 「이 주소에서 이 슬롯에 보여 줄 기본 화면」을 정해 둬야 합니다. 개발 중에는 클라이언트 이동만 하다가, 배포 후 사용자가 새로고침하면서 404 를 만나는 순서가 되기 쉽습니다.
렌더링 방식에 대한 제약도 하나 있습니다. 문서는 같은 세그먼트에서 한 슬롯이 동적이면 그 레벨의 모든 슬롯이 동적이 된다고 적습니다. 미리 만들어 둔 슬롯과 요청마다 만드는 슬롯을 같은 레벨에 섞을 수 없습니다.
인터셉트 라우트: 같은 주소, 다른 진입
인터셉트 라우트 문서는 목적을 이렇게 적습니다. 현재 레이아웃 안에서 새 라우트를 불러오면서 브라우저 주소는 바꾸는 것, 모달 같은 패턴에 쓰인다는 설명입니다.
문서의 예가 앞의 사진 화면입니다. 피드에서 사진을 누르면 /photo/123 을 가로채 피드 위에 모달로 덮습니다. 주소는 /photo/123 이 되므로 그대로 공유할 수 있습니다. 그리고 공유된 주소로 처음 들어오거나 새로고침하면 가로채기가 일어나지 않고 사진 페이지 전체가 렌더링됩니다.
규칙은 상대 경로를 닮은 괄호 표기입니다.
| 표기 | 가로채는 대상 |
|---|---|
(.) |
같은 레벨의 세그먼트 |
(..) |
한 단계 위 |
(..)(..) |
두 단계 위 |
(...) |
app 루트부터 |
피드 안에서 photo 세그먼트를 가로채려면 feed/(..)photo 폴더를 만듭니다. 이 표기는 파일 경로가 아니라 라우트 세그먼트를 기준으로 셉니다. 폴더 구조와 주소 구조가 다른 프로젝트에서는 이 점을 헷갈리기 쉽습니다.
두 규칙을 함께 쓰는 모달
실무에서 모달 패턴은 두 규칙을 함께 씁니다. 병렬 라우트로 모달용 슬롯(@modal)을 만들고, 그 안에서 인터셉트 라우트로 대상 페이지를 가로챕니다.
app/
├── layout.tsx ← children 과 modal 을 함께 렌더링
├── @modal/
│ ├── default.js ← 모달이 없을 때는 null
│ └── (.)photo/[id]/page.tsx
├── photo/[id]/page.tsx ← 직접 들어왔을 때의 전체 페이지
└── page.tsx@modal/default.js 가 null 을 돌려주면 평소에는 모달 자리가 비어 있습니다. 목록에서 사진을 누르면 가로챈 페이지가 그 슬롯에 들어가 모달이 뜹니다. 새로고침하면 슬롯은 default.js 로 돌아가고 photo/[id]/page.tsx 가 전체 화면으로 그려집니다.
쓰기 전에 생각할 것
이 두 규칙은 강력한 만큼 파일 구조가 복잡해집니다. 제가 보기에 판단 기준은 이렇습니다.
| 상황 | 필요한 것 |
|---|---|
| 모달을 열어도 주소가 그대로여도 되는가 | 평범한 state 로 충분 |
| 모달 상태를 주소로 공유해야 하는가 | 인터셉트 라우트 |
| 한 화면의 영역들이 각자 하위 페이지를 갖는가 | 병렬 라우트 |
| 새로고침했을 때 각 영역이 무엇을 보여 줄지 정했는가 | default.js |
마지막 줄을 먼저 답할 수 없다면 아직 병렬 라우트를 쓸 때가 아니라고 봅니다. 새로고침은 사용자가 가장 자주 하는 동작 중 하나이고, 그때 404 가 뜨는 것은 개발 중에 잘 보이지 않습니다.
여기까지가 확실한 부분
슬롯이 @폴더 규칙으로 만들어지고 주소에 나타나지 않는다는 점, children 이 암묵적 슬롯이라는 점, 새로고침 시 default.js 가 없으면 404 가 된다는 점, 한 슬롯이 동적이면 같은 레벨의 모든 슬롯이 동적이 된다는 점, 인터셉트 라우트의 괄호 표기와 공유 링크·새로고침에서는 가로채지 않는다는 점은 Next.js 16 문서 기준입니다. 모달 폴더 구조는 문서의 설명을 따라 구성한 예이고, 실제 프로젝트에서는 레이아웃 구조에 따라 표기 단계가 달라집니다.
함께 읽기
- Next.js 레이아웃과 라우트 그룹: loading.tsx 적용 범위Next.js 앱 라우터에서 한 폴더에는 이런 파일들이 함께 놓입니다.
- Next.js 데이터 가져오기 순서: 순차와 병렬, 그리고 Suspense 위치서버 컴포넌트에서 데이터를 읽는 코드는 이렇게 생깁니다.
- Next.js 다국어 경로 설계: 쿠키와 하위 경로공개하지 않고 내부에서만 여는 제품 카탈로그 데모에 언어 전환을 붙였습니다. 한국어, 영어, 중국어, 일본어, 독일어, 프랑스어 여섯 개입니다. 고른 언어는 쿠키에 저장하고, 쿠키가 없으면 브라우저의 Accept-Language 헤더를 봅니다. 주소는 나누지 않았습니다. /products/<기종> 하나로 여섯 언어를 모…
- Next.js 라우트 핸들러와 서버 함수 구분앱 라우터에서 서버로 무언가를 보내는 방법이 둘입니다. app/api/.../route.ts 에 라우트 핸들러를 만들거나, "use server" 를 붙인 서버 함수를 부르는 것입니다. 폼 하나를 붙일 때마다 어느 쪽을 쓸지 정해야 하는데, 「둘 다 서버에서 도니 아무거나」로 두면 나중에 갈립니다.
- Next.js 이미지 최적화: next/image와 빌드 때 굽기제품 카탈로그 데모를 만들면서 이미지 처리 방식을 정해야 했습니다. 화면에 나가는 사진은 제품 사진 몇 장과 로고뿐이고, 모두 저장소에 들어 있는 파일입니다. 요청마다 달라질 것이 없습니다.