

화면 내부에 여러 개의 레이아웃들을 렌더링하는 패턴.
Parallel Route는 복수의 '페이지들'을 한 화면에 렌더링되는 기법이다. 컴포넌트들이 아니다. 여러 컴포넌트들이 한 화면에 렌더링 되는건 굳이 따로 다룰 필요도 없다.

병렬로 렌더링되어야할 페이지 컴포넌트가 보관될 폴더를 뜻한다.
@feed, @sidebar 같이 @ 문자를 이름 가장 앞에 작성하여 구분한다.
App Router에서 폴더 내부에 page 파일을 두는것과 동일하지만, Parallel Route를 사용하는 경우에는 폴더 이름 제일 앞에 @ 문자가 추가되는 것.
slot에는 갯수 제한이 없다. 원하는 만큼 작성해서 사용하면 된다.



이런 식으로 slot 내부의 다른 페이지를 추가할 경우. 이 페이지 경로로 이동하였을 때 다른 Parallel Route 페이지들은 그대로 유지되지만, @feed의 페이지 내용이 하위 폴더의 setting 폴더의 페이지로 대체된다.
Parallel Route의 적용을 받은 layout의 경우, Route 적용을 위해 Props로 받아들이는 children는 해당 페이지로 진입하기 위한 URL 값이다.
parallel 폴더의 페이지들이니, URL 경로는 '/parallel'를 입력할 경우, children, sidebar, feed가 모두 존재하기 때문에 정상적으로 렌더링된다.
그런데 '/parallel/setting'의 경로를 입력해보면, feed 이외에는 해당되는 페이지가 존재하지 않기 때문에 이전 '/parallel'의 상태를 유지하게 된다.
따라서 '/parallel/setting' 경로를 입력하더라도, feed 이외의 부분들은 이전 경로의 상태를 유지하고. feed 부분만 하위 setting 폴더의 페이지로 대체된다.

이런 현상은 Link 태그를 이용하여 경로를 이동하였을 때만 발생한다. '/parallel'를 입력하지 않고, 바로 '/parallel/setting'를 입력하면 children, sidebar가 존재하지 않기 때문에 404 페이지로 이동하게 된다.
무조건 404 페이지로 이동하는게 싫다면, default.tsx 파일을 작성해주면 이 파일이 404 상황에서 이동될 페이지로 간주된다.
import Link from "next/link";
import { ReactNode } from "react";
export default function Layout({
children,
sidebar,
feed,
}: {
children: ReactNode;
sidebar: ReactNode;
feed: ReactNode;
}) {
return (
<div>
<div>
<Link href={"/parallel"}>parallel</Link>
<Link href={"/parallel/setting"}>parallel/setting</Link>
</div>
<br />
{sidebar}
{feed}
{children}
</div>
);
}
Slot 폴더 내부의 page 파일은 자동으로 부모 layout 컴포넌트로 Props의 형태로 전달된다.
(with-searchbar) Route Group과 동일하게, Slot 폴더는 URL 경로에는 아무 영향도 미치지 않는다. parallel/@feed와 같은 경로를 입력해봐야 404 페이지로 이동될 뿐.
Parallel Route는 Next 개발 모드에서 다소 불안정하게 동작한다. 문제가 발생할 경우, 개발 모드를 중단하고 프로젝트 폴더 내부의 .next 폴더를 삭제한 뒤, 재실행하면 보통 해결된다.


사용자가 동일한 경로에 접속할 때, 특정 조건 하에서라면 다른 페이지가 렌더링 되도록 하는 기술.
Intercepting Route은 초기 접속 요청이 아닐 때 동작한다.
클라이언트 사이드 렌더링, Link 컴포넌트, Router 객체의 push 등에서만 Intercepting Route이 동작할 수 있다.
SNS 등지에서 어느 게시물을 클릭하면, 기존에 보고 있던 페이지 위에 모달 형식으로 상세 페이지를 새로 띄워주는데 사용된다.
-> 상세 페이지를 끄면, 기존 목록 페이지가 다시 보여지는 식.
-> 이 상태에서 새로고침을 하면, 초기 접속이 되기 때문에 모달이 아니라 하나의 전체 페이지로 렌더링 되도록 하는 식으로 구현하곤 한다.
더 명확한 이해를 위해 공식문서를 참조해봐도 좋다.


book 페이지에 해당하는 Route 폴더 이름 그대로, 다만 '(.)' 문자를 제일 앞에 붙여주면 된다.
-> (.)book/[id] 폴더 내부의 page가 book/[id] 폴더 내부의 page의 Intercepting Route이 되는 것.
다만, (.)의 의미는 두 폴더가 같은 경로에 있다는 것이다. Intercepting의 대상이 상위 경로에 있다면 (..). App 폴더 바로 아래의 page를 Intercepting한다면 (...).
-> 상위의 상위 경로는? (..)(..).
import BookPage from "@/app/book/[id]/page";
import Modal from "@/components/modal";
export default function Page(props: any) {
return (
<div>
가로채기 성공!
<Modal>
<BookPage {...props} />
</Modal>
</div>
);
}

"use client";
import { ReactNode, useEffect, useRef } from "react";
import style from "./modal.module.css";
import { createPortal } from "react-dom";
import { useRouter } from "next/navigation";
export default function Modal({ children }: { children: 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();
}
}}
className={style.modal}
ref={dialogRef}
>
{children}
</dialog>,
document.getElementById("modal-root") as HTMLElement
);
}
createPortal? React-dom에서 제공되는 메소드.
첫번째 인자로는 렌더링 할 컴포넌트 (dialog 태그). 두번째 인자로는 해당 컴포넌트가 어느 위치에 렌더링되는지 기준이 될 DOM 요소를 넣어주면 된다. (document.getElementById("modal-root") as HTMLElement)
createPortal을 사용하지 않을 경우, Modal 컴포넌트의 내용은 특정 페이지의 하위 컴포넌트로 간주될 수 밖에 없다. CSS 설정 등에서 문제가 될 소지가 많아지는 것.
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>
<div id="modal-root"></div>
</body>
</html>
);
}
createPortal에서 사용한 modal-root라는 id값을 갖는 HTML 태그는 루트 레이아웃에 배치하는 것이 좋다.
Modal UI는 특정 조건 하에서 렌더링되거나 렌더링 되지 않아야 한다. useRef를 이용해서 Modal UI를 제어한 뒤, useEffect과 조건문을 사용해서 이를 제어해주면 된다.
onClick을 이용해서 Modal의 뒷배경을 클릭했을 때, Modal이 종료되도록 하는 이벤트를 넣는다.
-> 클릭한 대상의 nodeName을 기준으로 본문과 배경화면을 구분하면 된다.
-> 학습 프로젝트를 기준으로 배경화면은 dialog 태그. nodeName === "DIALOG"을 조건으로 사용한다.
ESC 키를 입력하거나 하는 상황을 상정하여 onClose 옵션에 Modal이 종료되도록 하는 이벤트를 설정한다.
뒤로가기의 기능은 router.back() 메소드를 사용한다.
기존 학습 프로젝트의 코드 상으로는, Modal이 출력되고 있음에도 기존 book 페이지가 뒷 배경으로 같이 렌더링 되는 문제가 있다.
정상적인 서비스의 모습에서는 book Modal이 출력될 때에는, index 페이지만이 출력되어야 한다.

export default function RootLayout({
children,
modal,
}: Readonly<{
children: React.ReactNode;
modal: 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>
);
}

