[Next.js] Parallel Routes & Intercepting Routes

Parallel Routes

URL 세그먼트를 담당하는 폴더에 @를 붙여 여러 페이지 경로를 렌더링할 수 있게하는 기능

아래 사진처럼 한 페이지에 두가지 페이지 요소를 띄우는 등의 작업을 할 수 있게 함

대시보드나 소셜 사이트의 피드, 상품 상세 페이지 모달 띄우기 같은 보여줘야하는 정보량이 많은 동적인 섹션에 유용하게 사용할 수 있음

 

모달 실습

1) (root)/@modal/page.tsx 생성

const ModalPage = () => {
    return (
        <div className="w-[300px] h-[300px] bg-blue-400">
            modal 영역
        </div>
    );
};

export default ModalPage;

 

2) layout의 props에 modal: React.ReactNode를 추가

export default function Layout({
  children,
  modal  // 추가
}: Readonly<{
  children: React.ReactNode;
  modal: React.ReactNode;  // 요기 추가
}>) {
  return (
    <>
     { * 생략 * }
     { modal }  { * 추가 * }
     { children }
    </>
  );
}

 

한 페이지에 동시에 다른 페이지가 렌더링되고 있는 것을 확인할 수 있음!

 

어떤 의미에서 평행하다는 걸까???

현재 @modal 은 (root)의 page.tsx와 같은 레벨의 경로이다. ((root)/page.tsx, (root)/@modal/page.tsx)

(root)에는 루트 페이지에서 타고들어갈 수 있는 product/[id]가 있는데, (root)/@modal/product/[id]를 생성해주면 (root)/product/[id] 와 같은 url로 인식되어 두 페이지를 병렬로 렌더링할 수 있게 된다!!!

따라서 (root)/product/[id]/page.tsx를 그대로 유지하면서 (root)/@modal/product/[id]/page.tsx에 정의한 UI를 추가로 띄울 수 있게 된다.

 

// (root)/@modal/product/[id]/page.tsx
const ProductDynamicPage = () => {
    return (
        <div className='w-[300px] h-[300px] bg-pink-300'>
            Product Detail에서 띄우고싶은 모달
        </div>
    );
};

export default ProductDynamicPage;

 

 

default.tsx

현재 product/[id]/page.tsx는 (root)와 (root)/@modal 이 동일하게 가지고 있지만, /cart/page.tsx는 루트 경로에만 있다.

이 상태에서 Link 태그를 이용해 소프트 네비게이션으로 이동하면, 문제가 없지만 만약 url 로 직접 접속하는 등 새로고침이 일어나는 경우 modal에대한 경로를 제대로 인식할 수 없어 404 오류가 발생한다.

 

이를 방지하기위해 모든 페이지를 병렬적으로 만들어줄수도 있겠지만, 페이지가 너무 많은 경우에는 너무 효율적이지 못한 작업이다. 이 때 @modal (parallel route)에 default.tsx를 생성해 해결할 수 있다!

// (root)/@modal/default.tsx
const Default = () => {
    return null;
};

export default Default;

 

이렇게 해주면 병렬적으로 선언되지 않은 모든 페이지에서 기본적으로 null을 반환하기 때문에 url로 접속 혹은 새로고침 (hard navigation) 하더라도 에러 없이 동작하는 것을 확인할 수 있다.

Cart 페이지 / Product 페이지

 

하지만 Link 태그로 soft navigation을 했을 때 의도하지 않은 모달이 떠있는 기묘한 현상이 발생한다.

 

분명 병렬적으로 선언하지 않은 다른 페이지에선 null을 반환하게 해뒀는데 Link로 이동하니까 이전 페이지의 modal이 뜬다

 

soft navigation 시 볼 수 있는 병렬적인 페이지와 hard navigation시 볼 수 있는 페이지를 구분하기위해 intersecting routes를 활용하는 경우가 많다.

 

 

Intersecting Routes

위의 예시에서 (root)/page.tsx에서 상품 목록 클릭시 -> 상품의 디테일 정보를 보여주는 모달을 띄우고 -> 모달에서 실제로 이동하는 버튼을 눌렀을 때 hadr navigation을 발생시켜 실제 디테일 페이지로 이동하는 예시를 구현해보자

 

1) 폴더구조

app
├── product
│   ├── [id]
│   │   └── page.tsx           <-- 실제 상품 상세 페이지 (product/[id])
├── @modal
│   └── (.)product
│       └── [id]
│           └── page.tsx       <-- 모달로 띄울 상품 정보 페이지
└── layout.tsx                 <-- 공통 레이아웃 (모달 포함)
└── page.tsx                   <-- 상품 목록 페이지

 

2) 공통 레이아웃에서 modal을 표시할 공간을 만들어준다

// app/layout.tsx
import { ReactNode } from 'react';

export default function RootLayout({
  children,
  modal,
}: {
  children: ReactNode;
  modal: ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <div>
          {modal}    {/* 모달을 렌더링할 영역 */}
          {children} {/* 메인 콘텐츠 영역 */}
        </div>
      </body>
    </html>
  );
}

 

3) (root)의 페이지에서 상품을 클릭했을 때 product 페이지로 이동하게 함

- 이 때 Link를 이용한 이동은 soft navigation에 해당하므로 @modal이 가로채 (root)/product/[id]/page.tsx가 아닌 (root)/@modal/(.)product/[id]/page.tsx로 이동한다

// app/page.tsx
import Link from 'next/link';

export default function ProductListPage() {
  // 생략
  
  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            {/* 클릭하면 모달로 상품 정보 표시 */}
            <Link href={`/product/${product.id}`}>
              {product.name} (Click for details in modal)
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

 

4) 모달에 클릭했을 때 실제 디테일 페이지로 이동하는 기능을 추가

- location.reload() 를 이용해 product/[id] url에서 새로고침하여 hard navigation이 일어나게 한다.

'use client'
import {useRouter} from "next/navigation";

const ProductDynamicPage = ({params}: {params: {id: string}}) => {
    const router = useRouter();

    const handleToPage = () => {
        location.reload();
    }

    const handleCloseModal = () => {
        router.back();
    }

    return (
        <div className='flex gap-5 fixed top-[50%] right-[50%] w-[300px] h-[300px] bg-pink-300'>
            Product Detail 모달
            <button className='rounded bg-amber-200' onClick={handleToPage}>상세 페이지로 이동</button>
            <button className='rounded bg-amber-200' onClick={handleCloseModal}>모달 닫기</button>
        </div>
    );
};

export default ProductDynamicPage;

 

'Next.js' 카테고리의 다른 글

[Next.js/Tanstack-Query] Next에서 prefetch하기  (0) 2024.10.01
[Next.js] Auth와 Middleware  (0) 2024.09.30
[Next.js] Caching  (1) 2024.09.28
[Next.js] Router Handler & Server Action  (0) 2024.09.27
[Next.js] Asset 최적화  (1) 2024.09.27