Ayden's journal

Parallel & Intercepting Routes

사람들이 page router를 버리고 app router로 갈아타는 데에는 수많은 이유가 있겠지만, 그 중 큰 비중을 차지하는 것이 바로 app router만의 routing 전략이지 싶다(하지만 나는 Route Groups Private Folders 같은 기능을 더 높게 친다). 실제로 주변 사람들에게 물어봤을 때 app router를 사용하는 주된 이유로 중첩 layout과 함께 고급 routing 전략을 꼽는 사람이 다수였다. Next.js 15 버전이 정식 출시된 이후로 app router도 ─ 물론 아직 버그가 한참 있기는 하지만 ─ 나름 쓸만해진 것 같아 각각의 라우트 전략을 간단히 살펴보고자 한다.

 

 

Parallel Routes

패럴렐 라우트 혹은 병렬 라우트를 사용하면 하나의 layout.tsx에서 하나 이상의 page.tsx를 동시에 ─ 혹은 조건부로 ─ 랜더링할 수 있다. Next.js는 이 기능을 대시보드나 게시물 피드 같이 동적인 섹션에서 사용하기를 권장하고 있다. 나는 같은 페이지에 존재해야되지만, 조금 결이 다른 컴포넌트를 랜더링해야할 때 패럴렐 라우트를 사용하는 편이다.

패럴렐 라우트는 @를 통해 명명된 파일 구조인 '슬롯'을 통해 구현한다. 아래의 이미지는 @team 슬롯과 @analytics 슬롯을 사용해 Layout.tsx에 세 개의 page.tsx를 병렬적으로 랜더링하는 모습이다. 슬롯은 부모 layout.tsx에 props로 전달되기 때문에 이를 사용하여 처리해주면 된다.

참고해야할 점은 슬롯 자체는 경로 세그먼트가 아니기에 URL 구조에 영항을 미치지 않는다는 사실이다. 또한 슬롯에는 ─ 일반적인 라우팅과 동일하게 ─ 동적 슬롯과 정적 슬롯이 존재하며, 동적 슬롯이 하나 이상 존재한다면 해당 경로의 모든 슬롯은 동적이어야 한다는 제한이 있다.

 

페이지를 탐색하는 방식에 따라 Next.js는 슬롯 내에서 랜더링되는 콘텐츠 유형을 다르게 가져간다. Link 컴포넌트나 router 객체의 push 메소드를 사용하는 '소프트 네비게이션'의 경우 Next.js는 부분 랜더링을 수행하여 URL과 일치하지 않더라도, 이전의 슬롯 상태를 유지하며 콘텐츠를 제공한다. 이는 '/' 페이지에서 '/dashboard' 페이지로 이동할 때, 슬롯 내에 해당하는 하위 page.tsx가 존재하지 않더라도 이전의 슬롯 상태를 계속 유지한다는 의미이다.

하지만 페이지 전체를 새로 고침해버리는 '하드 네비게이션'의 경우 Next.js는 현재 URL과 일치하지 않는 슬롯의 활성 상태를 결정할 수 없다. 따라서 default.tsx가 존재하지 않는 이상 404 페이지를 랜더링하게 된다. default.tsx는 위와 같은 상황에서 특정 슬롯의 백업 콘텐츠를 랜더링하는 역할을 한다. 특정 경로가 제대로 렌더링되지 않거나 부모 페이지 상태를 복원할 수 없을 때 페이지 전체를 faliure 시키는 대신, 해당 슬롯의 백업 콘텐츠를 렌더링하여 사용자 경험을 보호하는 역할을 한다.

 

특정 라우트의 기본 페이지가 되는 page.tsx 역시 Next.js는 @children으로 처리한다. 이를 공식 문서에서는 암시적 슬롯(implicit slot)이라고 한다. 이로 인해 Layout.tsx의 children props로 page.tsx의 내용이 들어오게 되는 것이다.

 

 

Intercepting Routes

인터셉팅 라우트 ─ 나는 긴빠이 라우트라고 부르기는 하는데 ─ 는 소프트 탐색 중에 특정 라우트에 대한 요청을 가로채어, 해당 라우트가 처리되는 방식을 제어한다. 패럴렐 라우트와 마찬가지로 소프트/하드 네비게이션에서 동작하는 방식이 다르기에 유저의 엔트리 포인트에 따라 다른 방식으로 페이지를 제공할 수 있다는 장점이 있다.

가로채고자 하는 라우트를 파일 시스템의 상대 경로와 비슷하게 (..) 와 같은 방법으로 표시하게 되는데, 주의해야할 점은 파일 시스템이 아니라 세그먼트에 대한 상대 경로로 작성해야 한다는 점이다. 따라서 2레벨 위의 세그먼트를 가로채기 위해서는 폴더 이름 앞에 (..)(..)를 붙여주어야 한다.

 

 

Parallel & Intercepting Routes

하지만 일반적으로 인터셉팅 라우트는 페럴렐 라우트와 '함께' 사용하여 딥 링크 ─ 특정 페이지나 상태로 바로 접근할 수 있는 링크 ─ 를 지원하는 모달을 만들 수 있다. 이를 통해 URL을 통해 모달 콘텐츠를 공유할 수 있고, 모달의 열림 상태를 state가 아니라 URL의 세그먼트로 관리하게 된다.

모달로 띄우고 싶은 라우트 전체를 @modal 슬롯 내부에서 처리하고, 평소에는 아무런 내용도 없는 default.tsx를 랜더링시킨 다음 필요한 라우트만 인터셉트 하는 방식이다. 만약 /report/[id] 라우트의 내용을 모달로 띄우고 싶다고 하면, 패럴랠 & 인터셉트 라우트의 형태는 @modal/(.)report/[id]와 같을 것이다.

// src/@modal/default.tsx

export default function Default() {
  return null
}
// src/layout.tsx
export default function RootLayout({
  children,
  modal
}: Readonly<{
  modal: ReactNode;
  children: ReactNode;
}>) {
  return (
    <html lang="ko">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
        {modal}
      </body>
    </html>
  );
}

 

Next.js의 모든 page.tsx는 컴포넌트를 default export 하기 때문에 어디서든 import 해올 수 있다. 덕분에 이렇게 인터셉팅 라우트에서 새로 컴포넌트를 꾸리는 것이 아니라 원래 라우트의 Page 컴포넌트를 가져와 간단히 재사용할 수 있다.

// src/@modal/(.)report/[id]/page.tsx

import ReportPage from "@/report/[id]/page"

export default function Page() {
  return (
    <Modal>
      <ReportPage />
    </Modal>
  );
}

 

 

결론

app router를 처음 접하는 사람들은 어째서인지 '모달을 구현하는 데 있어 반드시 패럴랠 라우터와 인터셉팅 라우터를 사용해야 한다'는 이상한 압박감을 느끼는 듯하다. 사실, 이 두 기능은 매우 유용하지만, 그 자체로 모든 문제를 해결하는 만능 열쇠는 아니다. 패럴랠 라우팅과 인터셉팅 라우팅은 복잡한 사용자 경험을 더 잘 관리할 수 있도록 돕는 도구일 뿐, 모달 구현을 위해서만 존재하는 것은 아니다.

 

내가 생각하기에 이 두 라우터를 사용해 모달을 구현하기에 적합한 경우는 ─ 인스타그램에서 게시물을 조회하면 모달이 뜨는 것과 같이 ─

사용자가 특정한 라우트를 소프트 네비게이팅으로 접근할 때와 하드 네비게이팅으로 접근할 때 서로 다른 컨텐츠를 랜더링해야하는 경우 말고는 없지 않나 싶다.

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기