Parallel Routes
병렬 라우팅이란 하나의 레이아웃에서 여러 페이지를 동시에 렌더링하는 것입니다.
(아래의 예시에서 team과 analytics, 2개의 page를 동시에 렌더링 중)
Convention
슬롯(@folder) 을 사용하여 생성하며, 동일 레벨의 layout에 props로 전달됩니다.
/*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는 파란색으로 표시하기 위해서는 다음과 같은 과정이 필요합니다.
URL의 pathname을 불러옵니다.
problem, clarification, standings가 URL에 포함되었는지 확인합니다.
확인 결과에 맞게 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의 장점은
페이지를 동시에 렌더링해서 초기 렌더링 속도가 빠르다
서버 컴포넌트에서 조건부 렌더링을 할 수 있다
이며, 불가피하게 사용하게 되는 클라이언트 컴포넌트는 말단으로 보내면 된다!
이상입니다.
참고자료
https://ariakit.org/examples/tab-next-router
https://nextjs.org/docs/app/building-your-application/routing/parallel-routes