Skip to main content

Command Palette

Search for a command to run...

[Next.js] Parallel Routes

병렬 라우팅을 이용한 Tabs 구현

Updated
3 min read

Parallel Routes

병렬 라우팅이란 하나의 레이아웃에서 여러 페이지를 동시에 렌더링하는 것입니다.
(아래의 예시에서 team과 analytics, 2개의 page를 동시에 렌더링 중)

Parallel Routes Diagram

Convention

슬롯(@folder) 을 사용하여 생성하며, 동일 레벨의 layout에 props로 전달됩니다.

Parallel Routes File-system Structure

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

추가로, children은 암묵적인 slot입니다.
따라서, app/page.tsx는 사실 app/@children/page.tsx 와 동일합니다.

이를 통해 알 수 있는 것은 다음과 같습니다
1. slot은 URL 구조에 영향을 주지 않습니다.
2. 동일 레벨의 layout에 prop으로 전달되며 동시에 렌더링이 가능합니다.

이것이 병렬 라우팅이며, 사용 방법의 전부입니다.

병렬 라우팅의 장점

  • 각 페이지마다 loading, error 페이지를 독립적으로 보여줄 수 있습니다
    -> 병렬로 연결된 페이지 중, 로딩이 완료된 페이지를 우선 표시할 수 있음
    -> 최초 로딩 성능 향상

  • 인증 상태 등과 같은 조건에 따라 렌더링할 수 있음

하지만 아래와 같은 상황에서는..?

Top, Problem, Clarification, Standings 로 이루어진 Tabs를 구현하는 중입니다.
각각의 버튼을 누르면 해당 탭의 컨텐츠를 표시합니다.

초록색이 [id]의 layout, 하늘색이 @tabs, 남색이 @tabs의 layout입니다.
'/[id]'일 때는 Top의 내용을
'/[id]/problem'일 때는 Problem의 내용을
'/[id]/clarification'일 때는 Clarification의 내용을
'/[id]/standings'일 때는 Standings의 내용을 보여줍니다.

하지만 여기서 한 가지 의문점이 생깁니다.
현재 slot은 @tabs 하나뿐인데 도대체 무엇과 병렬 라우팅을 하는 걸까요?
([id]의 page.tsx는 null을 반환하기 때문에 @children도 없는 상태입니다)

Tabs를 구현할 때 병렬 라우팅을 사용하는 이유는
약간의 성능 향상도 있겠지만
결국 SSR을 위해서입니다.

Top, Problem, Clarification, Standings를 각각 컴포넌트로 만들고
상태에 따라 조건부 렌더링을 하는 경우, 'use client'를 명시해야 합니다.
하지만 CSR의 경우 SEO에 불리합니다.

즉 병렬 라우팅의 또 다른 장점은
서버 컴포넌트에서도 조건부 렌더링을 할 수 있으며 SEO에 유리하다! 입니다.

Tabs 구현

// [id]/layout.tsx
export default async function Layout({
  params,
  tabs
}: {
  params: ContestDetailProps['params']
  tabs: React.ReactNode
}) {
    // 중략
    return (
      <article>
        <header className="flex justify-between p-5 py-8">
        {/* 중략 */}
        </header>
        {tabs}
      </article>
    )
  }
  return <p className="text-center">No Results</p>
}

tabs를 props로 받아서 {tabs}로 병렬 라우팅합니다.

// [id]/@tabs/layout.tsx
import ContestTabs from '../../_components/ContestTabs'
import type { ContestDetailProps } from '../layout'

export default function Layout({
  children,
  params
}: {
  children: React.ReactNode
  params: ContestDetailProps['params']
}) {
  const { id } = params
  return (
    <>
      <ContestTabs contestId={id}></ContestTabs>
      {children}
    </>
  )
}

활성화된 Link는 파란색으로 표시하기 위해서는 다음과 같은 과정이 필요합니다.

  1. URL의 pathname을 불러옵니다.

  2. problem, clarification, standings가 URL에 포함되었는지 확인합니다.

  3. 확인 결과에 맞게 Tab의 색을 파란색으로 바꿔줍니다.

위의 과정을 구현하기 위해서는 반드시 클라이언트 컴포넌트를 사용해야 합니다. 따라서 해당 부분을 말단으로 보내기 위해 ContestTabs를 컴포넌트화 했습니다.

'use client'

import { cn } from '@/lib/utils'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

export default function ContestTabs({ contestId }: { contestId: string }) {
  const id = contestId
  const pathname = usePathname()

  const isCurrentTab = (tab: string) => {
    if (tab === '') return pathname === `/contest/${id}`
    return pathname.startsWith(`/contest/${id}/${tab}`)
  }

  return (
    <div className="mb-4 flex justify-center gap-12">
      <Link
        href={`/contest/${id}`}
        className={cn(
          'text-lg text-gray-400',
          isCurrentTab('') && 'text-primary'
        )}
      >
        Top
      </Link>
      <Link
        href={`/contest/${id}/problem`}
        className={cn(
          'text-lg text-gray-400',
          isCurrentTab('problem') && 'text-primary'
        )}
      >
        Problem
      </Link>
      <Link
        href={`/contest/${id}/clarification`}
        className={cn(
          'text-lg text-gray-400',
          isCurrentTab('clarification') && 'text-primary'
        )}
      >
        Clarification
      </Link>
      <Link
        href={`/contest/${id}/standings`}
        className={cn(
          'text-lg text-gray-400',
          isCurrentTab('standings') && 'text-primary'
        )}
      >
        Standings
      </Link>
    </div>
  )
}

결론

이렇게 Parallel Routes를 사용하여 Tabs를 구현해보았습니다.
요약하자면 Parallel Routes의 장점은

  1. 페이지를 동시에 렌더링해서 초기 렌더링 속도가 빠르다

  2. 서버 컴포넌트에서 조건부 렌더링을 할 수 있다

이며, 불가피하게 사용하게 되는 클라이언트 컴포넌트는 말단으로 보내면 된다!

이상입니다.

참고자료

https://ariakit.org/examples/tab-next-router

https://nextjs.org/docs/app/building-your-application/routing/parallel-routes

More from this blog

[ZSH] tree 사용하기

들어가며 큰 규모의 프로젝트를 출시한 뒤, 후일을 위해서 더 늦기 전에 파일 정리 및 문서화를 진행해야했다. 문서화 작업을 하는 중에 기왕 정리하는 거 파일 구조를 이쁘게 트리 구조로 나열하여 코멘트를 달면 나중에 보더라도 이해하기 더 쉬울 것 같았다. 어떻게 해야 간지나는 트리 구조를 만들 수 있을까 방법을 찾다보니 역시나 파일 구조를 트리로 이쁘게 출력해주는 커맨드 툴이 존재했다. tree 커맨드에 대해서 알아보고 알짜배기 내용만 정리했다....

Feb 21, 20242 min read

[Next.js] parallel routes & intercepting routes

트위터 로그인 모달창을 만들어보며 넥스트의 parallel routes 와 intercepting routes 을 학습한 내용을 정리해보았습니다. 트위터 로그인 창을 확인해봅시다. 루트 디렉토리 화면을 배경으로 i/flow/login 페이지가 동시에 표시되고 있습니다. 저는 app router 를 학습하기 전까지는 createPortal 을 사용하여 포탈 영역에 로그인 컴포넌트를 띄우는 방식을 사용했었습니다. const NoLogin =()=...

Feb 1, 20244 min read

C/C++ 이진 트리(binary tree) 개요 및 구현(1)

개요 트리는 노드들이 나무 가지처럼 연결된 비선형 계층적 자료구조이다. 하위 트리가 존재하고, 그 노드에 또 하위 트리가 존재하는 자료구조 이다. 트리의 맨 위에 있는 루트 노드가 존재한다. 우리가 알아볼 트리는 이진 트리이다. 이진 트리는 자식 노드(부모로부터 아래로 이어진 노드)가 2개 이하인 구조를 말한다. 트리의 사용 사례로는 다음과 같다 계층 적 데이터 저장(파일,폴더) 효율적인 검색 속도 힙 데이터 베이스의 인덱싱 트리에 ...

Jan 31, 20244 min read

[React] Server component (RSC)

React.js 18 에 도입된 리액트 서버 컴포넌트는 서버에서 동작하는 리액트 컴포넌트를 의미합니다. Next가 권장하는 라우팅 방식인 app router의 기반이 되는 컴포넌트이기 때문에 app router 를 이해하기 위해서는 server component 에 대한 이해가 필요합니다. server component 리액트는 클라이언트단만을 컴포넌트화하는 대신, server component라는 개념을 통해 서버 영역을 컴포넌트화합니다. ...

Jan 29, 20243 min read

Flutter, JavaScript

42 posts