본문 바로가기
프론트엔드/Next.js

Next.js 13 Parallel Routes, Intercepting Routes

by ckstn0777 2023. 7. 22.

Overview

최근에 Next.js 13에서 새로운 Routes 기능이 생겼다는 사실을 알게되었습니다. Next.js 13.3 버전에서 새로 생겼다고 하네요. 이번 시간에는 Parallel Routes, Intercepting Routes에 대해 알아보고 활용법에 대해 생각해보고 간단한 실습을 하면서 마무리하도록 하겠습니다.

 

Parallel Routes

Introduction

공식 문서 : https://nextjs.org/docs/app/building-your-application/routing/parallel-routes

Parallel Routing을 사용하면 동일한 레이아웃에서 하나 이상의 페이지를 동시에 또는 조건부로 렌더링할 수 있습니다. 예를 들어 team 및 analytics 페이지를 동시에 렌더링할 수 있습니다.

Parallel Routing을 사용하면 개별적으로 스트리밍되는 각 경로에 대해 독립적인 오류 및 로드 상태를 정의할 수 있습니다. (오호... 신기합니다)

Parallel Routing을 사용하면 인증 상태와 같은 특정 조건에 따라 슬롯을 조건부로 렌더링할 수도 있습니다. 이렇게 하면 동일한 URL에서 완전히 구분된 코드를 사용할 수 있습니다.

Convention

Parallel routes는 명시적 slots을 사용하여 생성됩니다. 슬롯은 @folder 규약으로 정의되며, props 형태로 같은 레벨의 레이아웃으로 전달됩니다. 슬롯은 경로 세그먼트가 아니므로 URL 구조에 영향을 주지 않습니다. /@team/members 파일 경로는 /members에서 액세스할 수 있습니다.

예를 들어 다음 파일 구조는 두 개의 명시적 슬롯인 @analytics 및 @team을 정의합니다. (처음에 봤을때 '@'는 뭐지 싶었습니다...알고 보니 Parallel Routes 규칙이었죠 😂)

위의 폴더 구조는 app/layout.js의 컴포넌트가 @analytics 및 @team slots props을 수락하고, children props과 함께 이들 props을 병렬로 렌더링할 수 있음을 의미합니다.

export default function Layout(props: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {props.children}
      {props.team}
      {props.analytics}
    </>
  )
}

저도 한번 따라해봤는데 이상하게 Hot Reload가 제대로 안되더라고요...ㅠㅠ 새로 Parallel routes를 지정하면 한번 껐다가 pnpm run dev를 재시작해줘야 제대로 적용이 되었습니다. 아마 아직은 불안정한 상태인거 같네요.

어쨌거나 핵심은 @folder를 만든 다음, layout 파일 내에서 children props하는 것처럼 이들 props도 병렬로 렌더링할 수 있다는 것이군요. 재밌네요.

Unmatched Routes

기본적으로 슬롯 내에서 렌더링되는 내용은 현재 URL과 일치합니다. 일치하지 않는 슬롯의 경우, Next.js가 렌더링하는 내용은 라우팅 기법과 폴더 구조에 따라 다릅니다.

현재 URL을 기준으로 Next.js가 슬롯의 active 상태를 복구할 수 없을 때 fallback으로 렌더링 할 default.js 파일을 정의할 수 있습니다.

  • Soft Navigation : Next.js는 슬롯이 현재 URL과 일치하지 않더라도 슬롯의 이전 활성 상태를 렌더링합니다.
  • Hard Navigation : 전체 페이지를 다시 로드해야 하는 탐색인 하드 탐색에서 Next.js는 먼저 일치하지 않는 슬롯의 default.js 파일을 렌더링하려고 시도합니다. default.js가 정의되어있지 않다면 404가 렌더링됩니다.

예시 1

만약에서 @team 폴더에 settings 폴더를 만들고 page.tsx를 만든다고 가정해봅시다.

├── @analytics
│   └── page.tsx
├── @team
│   └── settings
│       └── page.tsx
├── layout.tsx
└── page.tsx

URL이 '/' 인 상태에서 새로고침을 한번 해볼까요? 새로고침은 Hard Navigation이라고 볼 수 있습니다. 그러면 404가 렌더링 되는 것을 볼 수 있습니다.

왜 그럴까요? 그것은 @team 폴더 내 settings 폴더 안에 page.tsx가 있기 때문입니다. 경로가 일치하지 않습니다. 따라서 이 문제를 해결하기 위해 @team 안에 default.tsx를 만들어주겠습니다.

├── @analytics
│   └── page.tsx
├── @team
│   ├── default.tsx ✅ 추가
│   └── settings
│       └── page.tsx
├── layout.tsx
└── page.tsx

default.tsx 파일은 아무것도 렌더링하지 않겠다는 뜻에서 null을 반환해줍니다.

export default function Default() {
  return null;
}

그러면 @analytics 에 해당 하는 부분만 보이고, @team/settings에 해당 하는 부분은 보이지 않는 것을 알 수 있습니다. 즉, default.tsx가 하는 역할은 경로가 일치하지 않더라도 404가 렌더링되지 않도록 해준다고 볼 수 있습니다.

예시 2

이번에는 '/' 를 '/settings' 로 Link를 통해 navigation을 시도하는 경우를 알아볼까요? 이는 Soft Navigation이라고 할 수 있습니다. 확인해보면 @analytics에는 settings가 폴더가 없기 때문에 현재 URL과 일치하지 않지만, Soft Navigation인 경우에는 슬롯의 이전 활성 상태를 렌더링하기 때문에 404가 렌더링 되는 대신 이전 상태를 보여줄 수 있습니다.

하지만, 새로고침을 통한 Hard Navigation이 발생하면 404 가 렌더링됩니다.

404가 안보이도록 만들려면 @analytics에 default.tsx라는 파일을 만들어주면 될 거 같습니다.

├── @analytics
│   ├── default.tsx ✅ 추가
│   └── page.tsx
├── @team
│   ├── default.tsx
│   └── settings
│       └── page.tsx
├── layout.tsx
└── page.tsx

아하... 그래도 404가 렌더링 됩니다. 저는 처음에 이 부분에서 한참 고민을 했습니다. 알고 보니 @team 내 settings 는 경로 세그먼트가 아니므로 URL 구조에 영향을 주지 않습니다. 즉, layout.tsx 내 children props에 대해서 생각해보면 아무것도 렌더링할 게 없는 것입니다.

이를 해결하려면 settings 폴더를 만들고 page.tsx를 만들거나 혹은 최상위에 default.tsx를 만들면 됩니다. 그래서 공식문서 내에서도 최상위에 default.tsx가 있었던 것이군요..ㅠㅠ

├── @analytics
│   ├── default.tsx 
│   └── page.tsx
├── @team
│   ├── default.tsx
│   └── settings
│       └── page.tsx
├── layout.tsx
├── default.tsx ✅ 추가
└── page.tsx

Examples - Modals

Parallel Routes 개념을 이용하면 쉽게 모달을 구현할 수 있습니다. 먼저 layout.tsx 에서 authModal props를 받아서 배치해줍니다.

// app/layout.tsx

export default async function Layout(props: {
  // ...
  authModal: React.ReactNode
}) {
  return (
    <>
      {/* ... */}
      {props.authModal}
    </>
  )
}

그리고 나서 @authModal 폴더 내에 login 폴더를 만들고 page.tsx를 작성해줍니다.

// app/@authModal/login/page.tsx

'use client'
import { useRouter } from 'next/navigation'
import { Modal } from 'components/modal'

export default async function Login() {
  const router = useRouter()
  return (
    <Modal>
      <span onClick={() => router.back()}>Close modal</span>
      <h1>Login</h1>
      ...
    </Modal>
  )
}

활성 상태가 아닐 때 404가 렌더링되는 것이 아닌 모달의 내용이 렌더링되지 않도록 하려면 null을 반환하는 default.js 파일을 만들 수 있습니다.

// app/@authModal/default.tsx

export default function Default() {
  return null
}

URL이 '/'가 '/login'로 Navigate 했지만 기존 화면 상태는 그대로이고, 그 위에 모달 창이 짠 하고 나타나게 됩니다.

그 외에도 Conditional Routes가 가능하니, 예를 들어 사용자가 로그인 되어있는 상태인지 아닌지 판단하여 조건부 렌더링도 해볼 수 있습니다.

 

Intercepting Routes

Introduction

공식 문서 : https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes

경로를 가로채면 현재 페이지의 컨텍스트를 유지하면서 현재 레이아웃 내에서 경로를 로드할 수 있습니다. 이 라우팅 패러다임은 특정 경로를 "차단"하여 다른 경로를 표시할 때 유용합니다.

예를 들어, 다음과 같은 구현이 가능합니다. 피드 내에서 사진을 클릭할 때 Next.js는 /feed 경로를 가로채고 이 URL을 "마스크"하여 /photo/123을 대신 표시합니다. 그리고 화면은 현재 페이지의 컨텍스트를 유지하면서 그 위에 모달이 사진과 함께 표시될 수 있습니다.

하지만, Hard Navigation을 하거나 경로를 직접적으로 접근할 때는 경로를 가로채지 않습니다. 따라서 모달 창이 보이는 대신 페이지가 보이게 만들 수 있습니다. (신기하네요...)

Convention

가로채기 경로는 (..) 규칙을 사용하여 정의할 수 있으며, 이는 상대 경로 규칙 ../과 유사하지만 세그먼트에 대한 것입니다.

  • (.) to match segments on the same level
  • (..) to match segments one level above
  • (..)(..) to match segments two levels above
  • (...) to match segments from the root app directory

예를 들어, (..)photo 디렉토리를 만들어 feed 세그먼트 내에서 photo 세그먼트를 가로챌 수 있습니다. 그니까 /feed 경로 내에 /photo/123 을 이동하려고 했을때 (..)photo 이기때문에 가로챌 수 있나 봅니다.

Note. (..) 규칙은 파일 시스템이 아니라 경로 세그먼트를 기반으로 합니다.

Examples - Modals

마지막으로 위에서 진행했던 Parallel Routes Modals 예시를 Intercepting Routes를 접목시켜서 개선해보겠습니다.

아래와 같이 구조를 만들어주겠습니다.

├── (auth)            // ✅ 추가 
│   └── sign-in
│       └── page.tsx
├── @authModal
│   ├── (.)sign-in    // ✅ 추가 
│   │   └── page.tsx
│   └── default.tsx
├── api
│   └── auth
│       └── [...nextauth]
│           └── route.ts
├── favicon.ico
├── layout.tsx
└── page.tsx
  • 메인 페이지에서 login 버튼을 클릭하면 /sign-in으로 이동하게 됩니다. 하지만 (.)sign-in 덕분에 경로를 인터셉트 당했습니다. 따라서 (auth)/sign-in/page.tsx가 렌더링되는 것이 아닌 @authModal Parallel Routes Modals 이 렌더링되게 됩니다.
  • 만약 사용자가 직접적으로 /sign-in 경로를 작성해서 진입하거나 혹은 /sign-in 내에서 새로고침을 한다면 (auth)/sign-in/page.tsx가 렌더링 됩니다.

어떠신가요? 버튼을 클릭하면 모달창이 나오고, 새로고침하거나 직접 경로를 접근하면 페이지가 나오는 형태... 사용자 경험에 있어서 나쁘지 않을거 같습니다.

 

마치면서

이번 시간에는 Next.js Routes 신규 기능인 Parallel Routes, Intercepting Routes에 대해 알아봤습니다. 자꾸 자고 일어나면 뭐가 생기는거 같은데 쉽지 않네요. 그래도 이번 신규 기능은 꽤 흥미로운거 같고 쓰임새도 많아보입니다.

아직 좀 불안정하고 개선할 포인트가 보이긴 하지만요.

 

참고 자료