태그
목차

병렬 라우트

생성일: 2024-03-04

수정일: 2024-03-04

병렬 라우트를 사용하면 동일한 레이아웃 내에서 하나 이상의 페이지를 동시에 또는 조건부로 렌더링할 수 있다. 대시보드나 소셜 사이트의 피드와 같이 앱의 매우 동적인 섹션에 유용하다.

예를 들어 대시보드를 고려할 때 병렬 라우트를 사용하여 team 페이지와 analytics 페이지를 동시에 렌더링할 수 있다:

슬롯

병렬 라우트는 명명된 슬롯(slot)을 사용하여 만들어진다. 슬롯은 @folder 규칙으로 정의된다. 예를 들어, 다음 파일 구조는 두 개의 슬롯을 정의한다: @analytics@team

슬롯은 공통 부모의 레이아웃에 프로퍼티로 전달된다. 위의 예제에서 app/layout.js 의 컴포넌트는 이제 @analytics@team 슬롯 프로퍼티를 허용하며, children 프로퍼티와 함께 병렬로 렌더링할 수 있다:

// app/layout.tsx
export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  );
}

그러나 슬롯은 라우트 세그먼트가 아니며 URL 구조에 영향을 미치지 않는다. 예를 들어, /dashboard/@analytics/views 의 경우 @analytics 가 슬롯이므로 URL은 /dashboard/views 가 된다.

Tip

children 프로퍼티는 폴더에 매핑할 필요가 없는 암시적 슬롯이다. 즉, app/page.jsapp/@children/page.js 와 동일하다.

활성 상태와 네비게이션

기본적으로 Next.js는 각 슬롯의 활성 상태(또는 하위 페이지)를 추적한다. 그러나 슬롯 내에서 렌더링되는 콘텐츠는 네비게이션 유형에 따라 달라진다:

네비게이션 유형 설명
소프트 네비게이션 클라이언트 사이드 네비게이션 중에 Next.js는 부분 렌더링을 수행하여 슬롯 내의 하위 페이지를 변경하는 동시에 다른 슬롯의 활성 하위 페이지가 현재 URL과 일치하지 않더라도 이를 유지한다.
하드 네비게이션 전체 페이지 로드(브라우저 새로 고침) 후 Next.js는 현재 URL과 일치하지 않는 슬롯의 활성 상태를 확인할 수 없다. 대신 일치하지 않는 슬롯에 대해 default.js 파일을 렌더링하거나 default.js 가 존재하지 않는 경우 404 를 렌더링한다.

Tip

일치하지 않는 경로에 대한 404 는 의도하지 않은 페이지에 실수로 병렬 라우트를 렌더링하는 것을 방지하는 데 도움이 된다.

default.js

초기 로드 또는 전체 페이지 리로드 중에 일치하지 않는 슬롯에 대한 폴백으로 렌더링할 default.js 파일을 정의할 수 있다.

다음 폴더 구조를 살펴보면 @team 슬롯에는 /settings 페이지가 있지만 @analytics 에는 없다.

/dashboard/settings 으로 이동할 때 @team 슬롯은 @analytics 슬롯의 현재 활성 페이지를 유지하면서 /settings 페이지를 렌더링한다.

새로 고침 시 Next.js는 @analytics 에 대한 default.js 를 렌더링한다. default.js 가 존재하지 않으면 404 가 대신 렌더링된다.

또한 children 은 암시적 슬롯이므로 Next.js가 상위 페이지의 활성 상태를 복구할 수 없는 경우 하위 항목에 대한 폴백을 렌더링하기 위해 default.js 파일도 만들어야 한다.

useSelectedLayoutSegments(s)

useSelectedLayoutSegmentuseSelectedLayoutSegments 는 모두 슬롯 내에서 활성 경로 세그먼트를 읽을 수 있는 parallelRoutesKey 파라미터를 받는다.

// app/layout.tsx
'use client';

import { useSelectedLayoutSegment } from 'next/navigation';

export default function Layout({ auth }: { auth: React.ReactNode }) {
  const loginSegments = useSelectedLayoutSegment('auth');
  // ...
}

사용자가 app/@auth/login(또는 URL 표시줄의 /login)으로 이동하면 loginSegments"login" 문자열과 동일하다.

예제

조건부 라우트

병렬 라우트를 사용하여 사용자 롤(role)과 같은 특정 조건에 따라 조건부로 라우트를 렌더링할 수 있다. 예를 들어 /admin 또는 /user 롤에 대해 다른 대시보드 페이지를 렌더링할 수 있다:

// app/dashboard/layout.tsx
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}</>;
}

탭 그룹

슬롯 안에 layout 을 추가하여 사용자가 슬롯을 독립적으로 네비게이션할 수 있도록 할 수 있다. 이 기능은 탭을 만들 때 유용하다.

예를 들어 @analytics 슬롯에는 두 개의 하위 페이지가 있다: /page-views/visitors.

@analytics 내에서 layout 파일을 만들어 두 페이지 간에 탭을 공유한다:

// app/dashboard/@analytics/layout.tsx
import Link from 'next/link';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/dashboard/page-views">Page Views</Link>
        <Link href="/dashboard/visitors">Visitors</Link>
      </nav>
      <div>{children}</div>
    </>
  );
}

모달

병렬 라우트를 인터셉트 라우트와 함께 사용하여 모달을 만들 수 있다. 이를 통해 모달을 만들 때 다음과 같은 일반적인 문제를 해결할 수 있다:

사용자가 클라이언트 사이드 네비게이션을 사용하여 레이아웃에서 로그인 모달을 열거나 별도의 /login 페이지에 액세스할 수 있는 다음 UI 패턴을 고려해보자:

이 패턴을 구현하려면 먼저 기본 로그인 페이지를 렌더링하는 /login 라우트를 생성한다.

// app/login/page.tsx
import { Login } from '@/app/ui/login';

export default function Page() {
  return <Login />;
}

그런 다음 @auth 슬롯에 null 을 반환하는 default.js 파일을 추가한다. 이렇게 하면 모달이 활성화되어 있지 않을 때 렌더링되지 않는다.

// app/@auth/default.tsx
export default function Default() {
  return null;
}

@auth 슬롯 내에서 /(.)login 폴더를 업데이트하여 /login 경로를 인터셉트한다. <Modal> 컴포넌트와 그 하위 컴포넌트를 /(.)login/page.tsx 파일로 가져온다:

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

Tip

  • 라우트를 인터셉트하는데 사용되는 규칙(예: (.) )은 파일 시스템 구조에 따라 다르다. 관련 내용은 라우트 인터셉트 규칙을 참조한다.
  • <Modal> 기능을 모달 콘텐츠( <Login> )에서 분리하면 모달 내부의 모든 콘텐츠(예: 폼)가 서버 컴포넌트인지 확인할 수 있다. 자세한 내용은 클라이언트 및 서버 컴포넌트 인터리빙 문서를 참조한다.

모달 열기

이제 Next.js 라우터를 활용하여 모달을 열고 닫을 수 있다. 이렇게 하면 모달이 열려 있을 때와 앞뒤로 네비게이션할 때 URL이 올바르게 업데이트된다.

모달을 열려면 @auth 슬롯을 부모 레이아웃에 프로퍼티로 전달하고 children 프로퍼티와 함께 렌더링한다.

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

@auth 슬롯을 더 이상 렌더링하지 않아야 하는 페이지에서 Link 컴포넌트를 사용하여 페이지를 떠날 때, 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/[...catchAll]/page.tsx
export default function CatchAll() {
  return null;
}

Tip

  • 앞서 설명한 동작("활성 상태와 네비게이션") 때문에 모달을 닫을 때 @auth 슬롯에 캐시-올 라우트를 사용한다. 더 이상 슬롯과 일치하지 않는 라우트에 대한 클라이언트 측 네비게이션이 계속 표시되므로 모달을 닫으려면 슬롯을 null 을 반환하는 라우트와 일치시켜야 한다.
  • 다른 예로는 갤러리에서 사진 모달을 열면서 전용 /photo/[id] 페이지도 함께 열거나 사이드 모달에서 쇼핑 카트를 여는 것 등이 있다.
  • 링크에서 예시를 확인한다.

다른 예로는 갤러리에서 사진 모달을 열면서 동시에 별도의 /photo/[id] 페이지를 가지고 있거나, 쇼핑 카트를 측면 모달에서 열 때가 있을 수 있습니다. Intercepted and Parallel Routes에서 모달의 예시를 확인하세요.

로딩 및 에러 UI

병렬 라우트를 독립적으로 스트리밍할 수 있으므로 각 라우트에 대해 독립적인 에러 및 로딩 상태를 정의할 수 있다:

자세한 내용은 로딩 UI에러 핸들링 문서를 참조한다.