문서
App Router 사용하기
병렬 라우트

병렬 라우트

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

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

병렬 라우트 다이어그램

슬롯

병렬 라우트는 명명된 슬롯을 사용하여 생성됩니다. 슬롯은 @folder 규칙을 사용하여 정의됩니다. 예를 들어, 다음 파일 구조는 @analytics@team이라는 두 개의 슬롯을 정의합니다:

병렬 라우트 파일 시스템 구조

슬롯은 공유 부모 레이아웃에 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}
    </>
  );
}
app/layout.js
export default function Layout({ children, team, analytics }) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  );
}

그러나 슬롯은 라우트 세그먼트아니며 URL 구조에 영향을 주지 않습니다. 예를 들어, /@analytics/views의 경우 URL은 /views가 됩니다. @analytics는 슬롯이기 때문입니다.

알아두면 좋은 점:

  • children prop은 폴더에 매핑될 필요가 없는 암시적 슬롯입니다. 이는 app/page.jsapp/@children/page.js와 동등함을 의미합니다.

활성 상태 및 네비게이션

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

  • 소프트 네비게이션: 클라이언트 측 네비게이션 중에 Next.js는 부분 렌더링을 수행하여 슬롯 내의 하위 페이지를 변경하면서 다른 슬롯의 활성 하위 페이지를 유지합니다. 이는 현재 URL과 일치하지 않더라도 마찬가지입니다.
  • 하드 네비게이션: 전체 페이지 로드(브라우저 새로 고침) 후, Next.js는 현재 URL과 일치하지 않는 슬롯의 활성 상태를 결정할 수 없습니다. 대신 일치하지 않는 슬롯에 대해 default.js 파일을 렌더링하거나, default.js가 없는 경우 404를 렌더링합니다.

알아두면 좋은 점:

  • 일치하지 않는 라우트에 대한 404는 의도하지 않은 페이지에 병렬 라우트를 실수로 렌더링하지 않도록 보장하는 데 도움이 됩니다.

default.js

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

다음 폴더 구조를 고려해보세요. @team 슬롯에는 /settings 페이지가 있지만 @analytics에는 없습니다.

병렬 라우트 일치하지 않는 라우트

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

새로 고침 시 Next.js는 @analytics에 대해 default.js를 렌더링합니다. default.js가 없으면 대신 404가 렌더링됩니다.

또한 children은 암시적 슬롯이므로 Next.js가 부모 페이지의 활성 상태를 복구할 수 없을 때 children에 대한 폴백을 렌더링하기 위해 default.js 파일을 생성해야 합니다.

useSelectedLayoutSegment(s)

useSelectedLayoutSegmentuseSelectedLayoutSegments 모두 parallelRoutesKey 매개변수를 받아 슬롯 내의 활성 라우트 세그먼트를 읽을 수 있습니다.

app/layout.tsx
"use client";
 
import { useSelectedLayoutSegment } from "next/navigation";
 
export default function Layout({ auth }: { auth: React.ReactNode }) {
  const loginSegment = useSelectedLayoutSegment("auth");
  // ...
}
app/layout.js
"use client";
 
import { useSelectedLayoutSegment } from "next/navigation";
 
export default function Layout({ auth }) {
  const loginSegment = useSelectedLayoutSegment("auth");
  // ...
}

사용자가 app/@auth/login(또는 URL 표시줄에서 /login)으로 이동하면 loginSegment는 문자열 "login"과 같아집니다.

예시

조건부 라우트

병렬 라우트를 사용하여 사용자 역할과 같은 특정 조건에 따라 라우트를 조건부로 렌더링할 수 있습니다. 예를 들어, /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}</>;
}
app/dashboard/layout.js
import { checkUserRole } from "@/lib/auth";
 
export default function Layout({ user, admin }) {
  const role = checkUserRole();
  return <>{role === "admin" ? admin : user}</>;
}

탭 그룹

슬롯 내에 layout을 추가하여 사용자가 슬롯을 독립적으로 탐색할 수 있게 할 수 있습니다. 이는 탭을 만드는 데 유용합니다.

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

두 개의 하위 페이지와 레이아웃이 있는 분석 슬롯

@analytics 내에 두 페이지 사이에 탭을 공유하기 위한 layout 파일을 생성합니다:

app/@analytics/layout.tsx
import Link from "next/link";
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/page-views">페이지 조회수</Link>
        <Link href="/visitors">방문자</Link>
      </nav>
      <div>{children}</div>
    </>
  );
}
app/@analytics/layout.js
import Link from "next/link";
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/page-views">페이지 조회수</Link>
        <Link href="/visitors">방문자</Link>
      </nav>
      <div>{children}</div>
    </>
  );
}

모달

병렬 라우트는 인터셉팅 라우트와 함께 사용하여 딥 링크를 지원하는 모달을 만들 수 있습니다. 이를 통해 모달을 만들 때 흔히 발생하는 다음과 같은 문제를 해결할 수 있습니다:

  • 모달 콘텐츠를 URL을 통해 공유 가능하게 만들기.
  • 페이지를 새로 고칠 때 모달을 닫는 대신 컨텍스트를 유지하기.
  • 이전 라우트로 이동하는 대신 뒤로 가기 네비게이션으로 모달 닫기.
  • 앞으로 가기 네비게이션으로 모달 다시 열기.

다음과 같은 UI 패턴을 고려해보세요. 사용자가 클라이언트 측 네비게이션을 사용하여 레이아웃에서 로그인 모달을 열거나 별도의 /login 페이지에 접근할 수 있습니다:

병렬 라우트 다이어그램

이 패턴을 구현하려면 먼저 주요 로그인 페이지를 렌더링하는 /login 라우트를 만듭니다.

병렬 라우트 다이어그램
app/login/page.tsx
import { Login } from "@/app/ui/login";
 
export default function Page() {
  return <Login />;
}
app/login/page.js
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 "...";
}
app/@auth/default.js
export default function Default() {
  return "...";
}

@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>
  );
}
app/@auth/(.)login/page.js
import { Modal } from "@/app/ui/modal";
import { Login } from "@/app/ui/login";
 
export default function Page() {
  return (
    <Modal>
      <Login />
    </Modal>
  );
}

알아두면 좋은 점:

  • 라우트를 가로채는 데 사용되는 규칙(예: (.))은 파일 시스템 구조에 따라 다릅니다. 인터셉팅 라우트 규칙을 참조하세요.
  • <Modal> 기능을 모달 콘텐츠(<Login>)와 분리함으로써 모달 내부의 모든 콘텐츠(예: )가 서버 컴포넌트가 되도록 할 수 있습니다. 자세한 내용은 클라이언트 및 서버 컴포넌트 교차를 참조하세요.

모달 열기

이제 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">모달 열기</Link>
      </nav>
      <div>{auth}</div>
      <div>{children}</div>
    </>
  );
}
app/layout.js
import Link from "next/link";
 
export default function Layout({ auth, children }) {
  return (
    <>
      <nav>
        <Link href="/login">모달 열기</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();
        }}
      >
        모달 닫기
      </button>
      <div>{children}</div>
    </>
  );
}
app/ui/modal.js
"use client";
 
import { useRouter } from "next/navigation";
 
export function Modal({ children }) {
  const router = useRouter();
 
  return (
    <>
      <button
        onClick={() => {
          router.back();
        }}
      >
        모달 닫기
      </button>
      <div>{children}</div>
    </>
  );
}

Link 컴포넌트를 사용하여 @auth 슬롯을 더 이상 렌더링하지 않아야 하는 페이지로 이동할 때, 병렬 라우트가 null을 반환하는 컴포넌트와 일치하는지 확인해야 합니다. 예를 들어, 루트 페이지로 돌아갈 때 @auth/page.tsx 컴포넌트를 만듭니다:

app/ui/modal.tsx
import Link from "next/link";
 
export function Modal({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Link href="/">모달 닫기</Link>
      <div>{children}</div>
    </>
  );
}
app/ui/modal.js
import Link from "next/link";
 
export function Modal({ children }) {
  return (
    <>
      <Link href="/">모달 닫기</Link>
      <div>{children}</div>
    </>
  );
}
app/@auth/page.tsx
export default function Page() {
  return "...";
}
app/@auth/page.js
export default function Page() {
  return "...";
}

또는 다른 페이지(예: /foo, /foo/bar 등)로 이동할 때 캐치올 슬롯을 사용할 수 있습니다:

app/@auth/[...catchAll]/page.tsx
export default function CatchAll() {
  return "...";
}
app/@auth/[...catchAll]/page.js
export default function CatchAll() {
  return "...";
}

알아두면 좋은 점:

  • 활성 상태 및 네비게이션에서 설명한 동작 때문에 @auth 슬롯에서 캐치올 라우트를 사용하여 모달을 닫습니다. 슬롯과 더 이상 일치하지 않는 라우트로의 클라이언트 측 네비게이션은 계속 표시되므로, 모달을 닫기 위해 null을 반환하는 라우트와 슬롯을 일치시켜야 합니다.
  • 다른 예로는 갤러리에서 사진 모달을 열면서도 전용 /photo/[id] 페이지를 갖거나, 측면 모달에서 쇼핑 카트를 여는 것이 있습니다.
  • 인터셉트된 라우트와 병렬 라우트를 사용한 모달의 예제를 확인하세요.

로딩 및 오류 UI

병렬 라우트는 독립적으로 스트리밍될 수 있어 각 라우트에 대해 독립적인 오류 및 로딩 상태를 정의할 수 있습니다:

병렬 라우트를 통해 사용자 정의 오류 및 로딩 상태가 가능합니다

자세한 정보는 로딩 UI오류 처리 문서를 참조하세요.