8. Parallel Routes (병렬 라우트)
병렬 라우트를 사용하면 동일한 레이아웃 내에서 여러 페이지를 동시에 또는 조건부로 렌더링할 수 있다. 이 기능은 대시보드, 소셜 사이트의 피드 등 매우 동적인 앱 섹션에 유용하다.
예를 들어, 대시보드에서 병렬 라우트를 사용하면 팀 페이지와 분석 페이지를 동시에 렌더링할 수 있다.
🔹 Slots (슬롯)
병렬 라우트는 슬롯(Slots) 이라는 개념을 사용해 구현 가능하다. 슬롯은 @폴더명
형식으로 정의된다.
예를 들어, 다음과 같은 폴더 구조는 @analytics
, @team
이라는 두 개의 슬롯을 정의한다.
app/
├── @analytics/
│ ├── page.js
├── @team/
│ ├── page.js
├── layout.js
├── page.js
이렇게 정의된 슬롯은 공유 부모 레이아웃(shared parent layout)의 props로 전달된다.
위의 예에서, app/layout.js
는 @analytics
, @team
슬롯을 props로 받아 children
prop과 함께 병렬로 렌더링할 수 있다.
예제: app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
📌 중요한 점
slots
은 라우트 세그먼트가 아니므로 URL에 영향을 주지 않는다.- 예를 들어,
/@analytics/views
의 URL은/views
이다.@analytics
는 URL에 포함되지 않는다.
- 예를 들어,
- 동적(
dynamic
) 슬롯과 정적(static
) 슬롯을 같은 레벨에서 사용할 수 없다.- 만약 하나의 슬롯이 동적 라우트라면, 해당 레벨의 모든 슬롯이 동적이어야 한다.
참고:
children
속성은 명시적으로 폴더에 매핑할 필요가 없는 암시적 슬롯(implicit slot) 이다.
즉,app/page.js
는app/@children/page.js
와 동일하다.
🔹 Active State와 네비게이션
기본적으로 Next.js는 각 슬롯의 활성 상태(Active State) 를 추적한다.
하지만, 페이지 탐색 방식(Soft Navigation vs Hard Navigation) 에 따라 렌더링 방식이 달라진다.
📌 Soft Navigation (클라이언트 측 이동)
👉 클라이언트 사이드 내비게이션 시 발생하는 동작
- Next.js는 페이지 전체를 새로고침하지 않고, 특정 부분(=슬롯 내의 서브페이지)만 부분적으로 변경(부분 렌더링(partial render))한다.
- 예를 들어, 페이지 내 여러 슬롯이 있을 때, 한 슬롯의 내용이 바뀌더라도 다른 슬롯의 기존 내용은 그대로 유지된다.
- 심지어 현재 URL과 일치하지 않더라도 기존 슬롯의 내용은 그대로 남아 있게 된다.
- ✅ 쉽게 말하면: 페이지 일부만 바꾸고, 나머지는 유지하는 방식
- 예)
/dashboard
에서/dashboard/settings
로 이동하면,@team
과@analytics
슬롯이 유지된다.
📌 Hard Navigation (새로고침 또는 직접 URL 입력)
👉 전체 페이지를 새로고침(브라우저 리프레시) 한 경우 발생하는 동작
- 새로고침 시 Next.js는 현재 URL과 일치하는 슬롯만 렌더링할 수 있다.
- 하지만 현재 URL과 일치하지 않는 슬롯이 있다면, Next.js는 그 슬롯의 활성 상태를 결정할 수 없다.
- 따라서 Next.js는 해당 슬롯에
default.js
파일이 있으면 그것을 렌더링하고,default.js
파일이 없으면 404 페이지(페이지를 찾을 수 없음) 를 표시한다. - ✅ 쉽게 말하면: 새로고침하면 Next.js는 기존 상태를 모르므로, 기본값을 사용하거나(=default.js), 없는 경우 404를 띄운다.
🔹 default.js
(기본 슬롯 컴포넌트)
슬롯이 처음 로드될 때 현재 URL에 일치하는 슬롯이 없다면, 해당 슬롯에 대한 기본 UI를 제공할 수 있다.
즉, Next.js가 슬롯의 활성 상태를 복구할 수 없을 때 default.js
가 대체 렌더링된다.
📌 예제
다음 폴더 구조에서 @team
슬롯에는 /settings
페이지가 있지만, @analytics
슬롯에는 대응되는 페이지가 없다.
app/
├── @team/
│ ├── settings/
│ │ └── page.js
│ └── page.js
├── @analytics/
│ ├── default.js <- 여기에 기본 UI 제공
│ └── page.js
├── default.js <- 여기에 기본 UI 제공
├── layout.js
└── page.js
- 사용자가
/settings
로 이동하면@team
슬롯은/settings
페이지를 렌더링한다. - 하지만
@analytics
슬롯은 현재 경로에 맞는 페이지가 없으므로,default.tsx
가 렌더링된다. - 만약
default.tsx
가 없다면,@analytics
슬롯은 404 에러 페이지를 표시한다. - 또한,
children
은 암시적 슬롯(implicit slot) 이므로, Next.js가 부모 페이지의 활성 상태를 복구할 수 없을 때를 대비해children
의 대체 렌더링을 위한default.js
파일을 생성해야 한다.
🔹 useSelectedLayoutSegment(s)
Next.js는 useSelectedLayoutSegment()
또는 useSelectedLayoutSegments()
를 제공하여, 현재 활성화된 슬롯의 라우트 세그먼트를 읽을 수 있다.
📌 예제: useSelectedLayoutSegment()
사용
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function Layout({ auth }: { auth: React.ReactNode }) {
const loginSegment = useSelectedLayoutSegment('auth')
// 현재 활성화된 auth 슬롯이 'login'인지 확인 가능
}
- 사용자가
/login
으로 이동하면,loginSegment
값은"login"
이 된다.
🔹 병렬 라우트 활용 예시
✅ 1. 조건부 라우트 (Conditional Routes)
병렬 라우트를 사용하면 사용자 역할(Role)에 따라 다른 페이지를 렌더링할 수 있다.
예를 들어, /admin
또는 /user
역할에 따라 다른 대시 보드 페이지를 렌더링할 수 있다.
📌 예제
import { checkUserRole } from '@/lib/auth'
export default function Layout({
user,
admin,
}: {
user: React.ReactNode
admin: React.ReactNode
}) {
const role = checkUserRole()
return role === 'admin' ? admin : user
}
- 관리자(Admin) 유저는
admin
슬롯을, 일반 유저는user
슬롯을 렌더링한다.
✅ 2. 탭 그룹 (Tab Groups)
탭 메뉴를 만들 때 각 탭이 독립적으로 탐색 가능하도록 병렬 라우트를 활용할 수 있다.
📌 예제
app/
├── @analytics/
│ └── page-views/
│ └── page.js
├── visitors/
│ └── page.js
└── layout.js
@analytics
내에서 두 페이지 사이의 탭을 공유 할 layout
파일을 생성한다.
import Link from 'next/link'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Link href="/page-views">Page Views</Link>
<Link href="/visitors">Visitors</Link>
</nav>
<div>{children}</div>
</>
)
}
@analytics
슬롯 내부에서/page-views
와/visitors
를 독립적으로 탐색할 수 있다.
✅ 3. 모달 (Modals)
병렬 경로(Parallel Routes)는 인터셉팅 라우트(Intercepting Routes) 와 함께 사용되어 깊은 연결(deep linking) 을 지원하는 모달을 생성할 수 있다.
이를 통해 모달을 구축할 때 발생할 수 있는 일반적인 문제를 해결할 수 있다.
- 모달 콘텐츠를 URL을 통해 공유할 수 있다.
- 페이지가 새로 고침 될 때 모달을 닫는 대신, 모달의 컨텍스트를 보존한다.
- 뒤로 가기(backwards navigation) 시, 이전 경로로 이동하는 대신 모달을 닫는다.
- 앞으로 가기(forwards navigation) 시, 모달을 다시 열 수 있다.
🚀 UI 패턴 예시
사용자가 클라이언트 측 탐색을 사용하여 레이아웃에서 로그인 모달을 열거나, 별도의 /login
페이지에 접근할 수 있는 UI 패턴을 고려해보자.
이 패턴을 구현하려면, 먼저 기본 로그인 페이지를 렌더링하는 /login
경로를 생성해야 한다.
app/login/page.tsx
import { Login } from '@/app/ui/login';
export default function Page() {
return <Login />;
}
그런 다음 @auth
슬롯 내부에 default.js
파일을 추가하여 null
을 반환한다. 이렇게 하면 모달이 활성화되지 않았을 때 모달이 렌더링되지 않도록 할 수 있다.
app/@auth/default.tsx
export default function Default() {
return null;
}
@auth
슬롯 내부에서 /login
경로를 가로채기 위해, (.)login
폴더를 업데이트한다.<Modal>
컴포넌트와 그 자식 컴포넌트를 (.)login/page.tsx
파일에 import한다.
app/@auth/(.)login/page.tsx
import { Modal } from '@/app/ui/modal';
import { Login } from '@/app/ui/login';
export default function Page() {
return (
<Modal>
<Login />
</Modal>
);
}
참고
- 경로를 가로채기 위한 협약은 파일 시스템 구조에 따라 다르다. -> Intercepting Routes convention 참조
<Modal>
기능과 모달 콘텐츠(<Login>
)를 분리하면, 모달 내부의 콘텐츠(예: forms)가 서버 컴포넌트로 처리된다. -> Interleaving Client and Server Components 참조
모달 열기
이제 Next.js 라우터를 활용하여 모달을 열고 닫을 수 있다. 이렇게 하면 모달이 열릴 때 URL이 올바르게 업데이트되고, 뒤로 가기 및 앞으로 가기에서 모달이 정확히 동작한다.
모달을 열려면, @auth
슬롯을 부모 레이아웃에 prop으로 전달하고, children
prop과 함께 렌더링하면 된다.
app/layout.tsx
import Link from 'next/link';
export default function Layout({
auth,
children,
}: {
auth: React.ReactNode;
children: React.ReactNode;
}) {
return (
<>
<nav>
<Link href="/login">Open modal</Link>
</nav>
<div>{auth}</div>
<div>{children}</div>
</>
);
}
사용자가 <Link>
를 클릭하면 /login
페이지로 이동하는 대신 모달이 열린다.
하지만 새로고침 또는 초기 로드 시 /login
경로로 이동하면 사용자가 기본 로그인 페이지로 이동한다.
모달 닫기
모달은 router.back()
을 호출하거나 <Link>
컴포넌트를 사용하여 닫을 수 있다.
app/ui/modal.tsx
'use client';
import { useRouter } from 'next/navigation';
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter();
return (
<>
<button onClick={() => router.back()}>Close modal</button>
<div>{children}</div>
</>
);
}
/login
외의 페이지로 이동 시 모달 닫기
Link
컴포넌트를 사용하여 @auth
슬롯을 더 이상 렌더링하지 않아야 하는 페이지로 이동하는 경우, 병렬 경로(parallel route)가 null
을 반환하는 컴포넌트와 일치해야 한다.
예를 들어, 루트 페이지(/
)로 이동하면 @auth/page.tsx
가 실행되도록 만들고, 이 파일에서 null을 반환하여 모달이 사라지도록 한다.
app/ui/modal.tsx
import Link from 'next/link';
export function Modal({ children }: { children: React.ReactNode }) {
return (
<>
<Link href="/">Close modal</Link>
<div>{children}</div>
</>
);
}
app/@auth/page.tsx
export default function Page() {
return null;
}
Catch-All 경로 사용
또는 다른 페이지(예: /foo
, /foo/bar
등)로 이동하는 경우, catch-all 슬롯을 사용할 수 있다.
app/@auth/[...catchAll]/page.tsx
export default function CatchAll() {
return null;
}
참고
@auth
슬롯에서 모달을 닫기 위해 catch-all 라우트를 쓰는 이유는 Active state and navigation에서 설명한 동작 방식 때문이다.- 클라이언트 사이드 내비게이션을 할 때, 원래
@auth
슬롯이 적용되던 경로에서 벗어나도 모달이 그대로 남아있을 수 있다.- 따라서 모달을 확실하게 닫으려면,
@auth
슬롯이 특정 경로와 매칭되었을 때null
을 반환하도록 만들어야 한다. 이를 이용해 모달을 화면에서 사라지게 할 수 있다.
기타 예시
- 갤러리에서 사진 모달을 열 때, 전용
/photo/[id]
페이지를 띄우기 - 측면(side) 모달에서 쇼핑 카트를 열 때에도 이와 비슷한 방식을 적용 할 수 있음
- 병렬 경로 및 가로채기 경로를 사용하여 모달을 구현한 예시로 다음 링크를 참고 -> 구현예시
🔹 로딩 및 에러 UI
각 슬롯은 개별적으로 스트리밍될 수 있으며, 각 슬롯마다 별도의 로딩/에러 UI를 설정할 수 있다.
📌 정리
✅ 병렬 라우트는 동적인 UI(대시보드, 탭, 모달 등)를 구현하는 데 유용한 기능이다.
https://nextjs.org/docs/app/building-your-application/routing/parallel-routes
Routing: Parallel Routes | Next.js
Simultaneously render one or more pages in the same view that can be navigated independently. A pattern for highly dynamic applications.
nextjs.org
'웹 프로그래밍 > Next.js' 카테고리의 다른 글
[Next.js 공식문서 정리] Routing : 09. Intercepting Routes (0) | 2025.03.29 |
---|---|
[Next.js 공식문서 정리] Routing : 07. Dynamic Routes.md (0) | 2025.03.29 |
[Next.js 공식문서 정리] Routing : 06. Route Groups (0) | 2025.03.29 |
[Next.js 공식문서 정리] Routing : 05. Redirecting (0) | 2025.03.29 |
[Next.js 공식문서 정리] Routing : 04. Loading UI and Streaming (2) | 2025.03.29 |
[Next.js 공식문서 정리] Routing : 03. Error Handling (0) | 2025.03.29 |