본문 바로가기
웹 프로그래밍/Next.js

[Next.js 공식문서 정리] Routing : 08. Parallel Routes

by 청량리 물냉면 2025. 3. 29.
반응형

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.jsapp/@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>
  );
}

참고

 

모달 열기

이제 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

 

반응형