Section 9. 고급 라우팅 패턴

OlMinJe·2025년 10월 22일

Next.js

목록 보기
19/20
post-thumbnail

Parallel Route (병렬 라우트)

하나의 화면에 여러 개의 페이지 컴포넌트들를 동시에 렌더링하는 패턴

공식문서

Parallel 폴더 구조

Parallel Route는 @슬롯이름 폴더(= Slot)를 만들어 쓰고,
이 슬롯들은 부모 레이아웃의 props로 주입된다.

예를 들어 @sidebar 슬롯이 있다면 레이아웃에서는 이렇게 받는다:

export default function Layout({
  children,
  sidebar,
}: {
  children: React.ReactNode;
  sidebar: React.ReactNode;
}) {
  return (
    <div>
      {sidebar}
      {children}
    </div>
  );
}

💡 Slot은 URL에 영향을 주지 않는다.
Route Group처럼 경로에는 나타나지 않고, 레이아웃에 “추가로 끼워 넣을 페이지”를 전달하는 방식으로,
children은 기본 페이지 슬롯이므로 따로 폴더가 없어도 자동으로 주입된다.

Parallel 적용 결과 화면

이처럼 Next의 페럴렐은 여러 개의 페이지 컴포넌트를 하나의 화면에 병렬로 렌더링 시켜주는 기능이다.


Slot 안에 새로운 페이지 생성하기

parallel/@feed/setting/page.tsx

export default function Page() {
  return <div>@feed/setting</div>;
}

부모 레이아웃에서 여러 슬롯을 동시에 받으면 이렇게 적용하면 된다.

import Link from 'next/link';

export default function Layout({
  children,
  sidebar,
  feed,
}: {
  children: React.ReactNode;
  sidebar: React.ReactNode;
  feed: React.ReactNode;
}) {
  return (
    <div>
      <div>
        <Link href={'/parallel'}>parallel</Link>
        &nbsp;
        <Link href={'/parallel/setting'}>parallel/setting</Link>
      </div>
      {sidebar}
      {children}
      {feed}
    </div>
  );
}

parallel setting

슬롯 내의 페이지로 이동할 때 layout.tsx의 props의 값은 어떻게 될까?


슬롯 전환 시 동작 포인트!

  • feed 슬롯의 경우 하위 페위지가 존재하므로 해당 UI가 렌더링된다
  • sidebar 슬롯에 해당 경로의 페이지가 없으면 404가 될 수 있는데, "직전 슬롯 렌더링 결과"를 재사용해 깜빡임을 줄여준다.
    이는 클라이언트 네비게이션(Link, push)에만 해당되며, 새로고침의 경우 초기 렌더링이므로 이전 결과가 없어 404가 보일 수 있다.

default.tsx

슬롯의 안전망
Slot에 매칭되는 페이지가 없을 대 보여줄 기본 UI를 제공하는 파일이다.

export default function Default() {
  return <div>/parallel/default</div>;
}

이렇게 하면 네비게이션/새로고침 상관없이 항상 슬롯 자리를 안전하게 채워준다.

parallel default


Intercepting Route(인터셉팅 라우트)

"같은 경로를 가더라도, 상황에 따라 다른 UI로 가로채기"
동일한 경로에 접속하더라도 특정 조건을 만족하면, 그때는 원래 페이지가 아닌 다른 페이지를 렌더링 하도록 설정하는 기능


📌 폴더명 규칙 요약

  • (.) : 동일 수준 경로의 페이지를 가로챔
  • (..) : 한 단계 상위 경로 기준으로 탐색
  • (..)(..) : 두 단계 상위 …
  • (...) : app 바로 아래 기준
// app/(.)book/[id]/page.tsx
export default function Page() {
  return <div>가로채기 성공!</div>;
}

Intercepting Route

도서 상세를 불러오기 위해 기존에 사용하던 페이지의 컴포넌트를 import 해주고, props를 전달해준다.
이때 props는 인터셉터 과정에서 똑같이 전달되기 때문에 그대로 전달해준다.

import BookPage from '@/app/book/[id]/page';

export default function Page(props: any) {
  return (
    <div>
      가로채기 성공!
      <BookPage {...props} />
    </div>
  );
}

모달로 띄우기

'use client';

import style from '@/components/modal.module.css';
import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

export default function Modal({ children }: { children: React.ReactNode }) {
  const dialogRef = useRef<HTMLDialogElement>(null);
  const router = useRouter();

  useEffect(() => {
    if (!dialogRef.current?.open) {
      dialogRef.current?.showModal();
      dialogRef.current?.scrollTo({
        top: 0,
      });
    }
  }, []);

  return createPortal(
    <dialog
      onClose={() => router.back()}
      onClick={(e) => {
        // 모달의 배경이 클릭된거면 뒤로가기
        if ((e.target as any).nodeName === 'DIALOG') {
          router.back();
        }
      }}
      ref={dialogRef}
      className={style.modal}
    >
      {children}
    </dialog>,
    document.getElementById('modal-root') as HTMLElement
  );
}

루트 레이아웃에 아래와 같이 모달 요소를 작성한다.

//...
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <div className={style.container}>
          <header>
            <Link href={'/'}>📚 ONEBITE BOOKS</Link>
          </header>
          <main>{children}</main>
          <Footer />
        </div>
      </body>
      <div id="modal-root"></div>
    </html>
  );
}

Intercepting & Parallel Route: @modal 슬롯

도서 리스트는 그대로 두고, 상세는 모달 슬롯으로 병렬 렌더링하기!

// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: Readonly<{
  children: React.ReactNode;
  modal: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <div className={style.container}>
          <header>
            <Link href={'/'}>📚 ONEBITE BOOKS</Link>
          </header>
          <main>{children}</main>
          <Footer />
        </div>
        {modal}
        <div id="modal-root"></div>
      </body>
    </html>
  );
}

app/@modal/(.)book/[id]/page.tsx에 작업한 도서 상세 슬롯을 옮겨서, 도서 상세 모달 뒷 배경으로 북 리스트가 보이는 메인 페이지가 보이도록 해보자.

그리고 @modal 경로에 default.tsx 페이지도 함께 만들어준다!

(.)book/[id]를 가로채고, 모달 슬롯에 렌더링하기

Intercepting & Parallel Route

✅ 정리하며

  • Parallel Route: 여러 페이지를 동시에 렌더링 (Slot은 레이아웃 props로 전달됨)
  • default.tsx: 슬롯에 페이지가 없을 때 기본 UI 제공
  • Intercepting Route: 같은 경로라도 클라이언트 네비게이션이면 가로채어 다른 UI(예: 모달)로 렌더링
profile
큐트걸

0개의 댓글