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

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은 기본 페이지 슬롯이므로 따로 폴더가 없어도 자동으로 주입된다.

이처럼 Next의 페럴렐은 여러 개의 페이지 컴포넌트를 하나의 화면에 병렬로 렌더링 시켜주는 기능이다.
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>
<Link href={'/parallel/setting'}>parallel/setting</Link>
</div>
{sidebar}
{children}
{feed}
</div>
);
}

슬롯 내의 페이지로 이동할 때 layout.tsx의 props의 값은 어떻게 될까?
feed 슬롯의 경우 하위 페위지가 존재하므로 해당 UI가 렌더링된다sidebar 슬롯에 해당 경로의 페이지가 없으면 404가 될 수 있는데, "직전 슬롯 렌더링 결과"를 재사용해 깜빡임을 줄여준다.Link, push)에만 해당되며, 새로고침의 경우 초기 렌더링이므로 이전 결과가 없어 404가 보일 수 있다.default.tsx슬롯의 안전망
Slot에 매칭되는 페이지가 없을 대 보여줄 기본 UI를 제공하는 파일이다.
export default function Default() {
return <div>/parallel/default</div>;
}
이렇게 하면 네비게이션/새로고침 상관없이 항상 슬롯 자리를 안전하게 채워준다.

"같은 경로를 가더라도, 상황에 따라 다른 UI로 가로채기"
동일한 경로에 접속하더라도 특정 조건을 만족하면, 그때는 원래 페이지가 아닌 다른 페이지를 렌더링 하도록 설정하는 기능
(.) : 동일 수준 경로의 페이지를 가로챔(..) : 한 단계 상위 경로 기준으로 탐색(..)(..) : 두 단계 상위 …(...) : app 바로 아래 기준// app/(.)book/[id]/page.tsx
export default function Page() {
return <div>가로채기 성공!</div>;
}

도서 상세를 불러오기 위해 기존에 사용하던 페이지의 컴포넌트를 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>
);
}
@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]를 가로채고, 모달 슬롯에 렌더링하기

default.tsx: 슬롯에 페이지가 없을 때 기본 UI 제공