Parallel Routes
Parallel Routes를 사용하면 동일한 레이아웃 내에서 하나 이상의 페이지를 동시에 또는 조건부로 렌더링할 수 있습니다. 이는 소셜 사이트의 대시보드 및 피드와 같은 매우 동적인 앱 섹션에 유용합니다.
예를 들어, 대시보드를 고려할 때, Parallel Routes를 사용하여 team
및 analytics
페이지를 동시에 렌더링할 수 있습니다:
Slots
Parallel Routes는 slots라는 이름으로 생성됩니다. Slots는 @folder
규칙으로 정의됩니다. 예를 들어, 다음 파일 구조는 두 개의 slots인 @analytics
와 @team
을 정의합니다:
Slots는 공유된 부모 레이아웃에 props로 전달됩니다. 위의 예에서는 app/layout.js
의 컴포넌트가 이제 @analytics
및 @team
slots props를 받아들이고, children
prop과 함께 병렬로 렌더링할 수 있습니다:
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
export default function Layout({ children, team, analytics }) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
하지만, slots는 route segments가 아니며 URL 구조에 영향을 미치지 않습니다. 예를 들어, /@analytics/views
의 경우 URL은 @analytics
가 slot이므로 /views
가 됩니다.
알아두면 좋은 점:
children
prop은 폴더에 매핑될 필요가 없는 암묵적 slot입니다. 이는app/page.js
가app/@children/page.js
와 동일함을 의미합니다.
Active state and navigation
기본적으로 Next.js는 각 slot에 대한 활성 state (또는 하위 페이지)를 추적합니다. 그러나 slot 내에서 렌더링되는 콘텐츠는 탐색 유형에 따라 달라집니다:
- Soft Navigation: 클라이언트 측 탐색 중에 Next.js는 부분 렌더링을 수행하여 현재 URL과 일치하지 않더라도 다른 slot의 활성 하위 페이지를 유지하면서 slot 내의 하위 페이지를 변경합니다.
- Hard Navigation: 전체 페이지 로드(브라우저 새로 고침) 후 Next.js는 현재 URL과 일치하지 않는 slots의 활성 상태를 결정할 수 없습니다. 대신, 일치하지 않는 slots에 대해
default.js
파일을 렌더링하거나,default.js
가 없으면404
를 렌더링합니다.
알아두면 좋은 점:
- 일치하지 않는 경로에 대한
404
는 의도하지 않은 페이지에 Parallel Route가 렌더링되지 않도록 보장합니다.
default.js
초기 로드 또는 전체 페이지 새로 고침 시 일치하지 않는 slots에 대한 대체 렌더링을 위해 default.js
파일을 정의할 수 있습니다.
다음 폴더 구조를 고려하세요. @team
slot에는 /settings
페이지가 있지만, @analytics
에는 없습니다.
/settings
로 이동할 때, @team
slot은 /settings
페이지를 렌더링하면서 @analytics
slot의 현재 활성 페이지를 유지합니다.
새로 고침 시, Next.js는 @analytics
에 대해 default.js
를 렌더링합니다. default.js
가 없으면 대신 404
가 렌더링됩니다.
추가로, children
은 암묵적 slot이므로 Next.js가 부모 페이지의 활성 상태를 복구할 수 없을 때 children
에 대한 대체 렌더링을 위해 default.js
파일을 생성해야 합니다.
useSelectedLayoutSegment(s)
useSelectedLayoutSegment
및 useSelectedLayoutSegments
는 parallelRoutesKey
매개변수를 받아 slot 내의 활성 라우트 세그먼트를 읽을 수 있게 합니다.
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function Layout({ auth }: { auth: React.ReactNode }) {
const loginSegment = useSelectedLayoutSegment('auth')
// ...
}
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function Layout({ auth }) {
const loginSegment = useSelectedLayoutSegment('auth')
// ...
}
사용자가 app/@auth/login
(또는 URL 바의 /login
)으로 이동하면, loginSegment
는 문자열 "login"
과 동일합니다.
Examples
Conditional Routes
Parallel Routes를 사용하여 사용자 역할과 같은 특정 조건에 따라 라우트를 조건부로 렌더링할 수 있습니다. 예를 들어, /admin
또는 /user
역할에 대해 다른 대시보드 페이지를 렌더링하려면:
import { checkUserRole } from '@/lib/auth'
export default function Layout({
user,
admin,
}: {
user: React.ReactNode
admin: React.ReactNode
}) {
const role = checkUserRole()
return <>{role === 'admin' ? admin : user}</>
}
import { checkUserRole } from '@/lib/auth'
export default function Layout({ user, admin }) {
const role = checkUserRole()
return <>{role === 'admin' ? admin : user}</>
}
Tab Groups
사용자가 slot을 독립적으로 탐색할 수 있도록 slots 내에 layout
을 추가할 수 있습니다. 이는 탭을 만드는 데 유용합니다.
예를 들어, @analytics
slot에는 두 개의 하위 페이지 /page-views
와 /visitors
가 있습니다.
@analytics
내에 layout
파일을 생성하여 두 페이지 간에 탭을 공유하게 합니다:
import Link from 'next/link'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Link href="/page-views">Page Views</Link>
<Link href="/visitors">Visitors</Link>
</nav>
<div>{children}</div>
</>
)
}
import Link from 'next/link'
export default function Layout({ children }) {
return (
<>
<nav>
<Link href="/page-views">Page Views</Link>
<Link href="/visitors">Visitors</Link>
</nav>
<div>{children}</div>
</>
)
}
Modals
Parallel Routes는 Intercepting Routes와 함께 사용하여 딥 링크를 지원하는 모달을 생성할 수 있습니다. 이를 통해 모달을 구축할 때 일반적으로 발생하는 문제를 해결할 수 있습니다:
- 모달 콘텐츠를 URL을 통해 공유 가능하게 만들기.
- 페이지를 새로 고침할 때 컨텍스트를 유지하여 모달을 닫지 않기.
- 이전 탐색 시 모달 닫기.
- 앞으로 탐색 시 모달 다시 열기.
사용자가 클라이언트 측 탐색을 사용하여 레이아웃에서 로그인 모달을 열거나 별도의 /login
페이지에 접근할 수 있는 다음 UI 패턴을 고려해보세요:
이 패턴을 구현하려면, 먼저 주요 로그인 페이지를 렌더링하는 /login
라우트를 생성합니다.
import { Login } from '@/app/ui/login'
export default function Page() {
return <Login />
}
import { Login } from '@/app/ui/login'
export default function Page() {
return <Login />
}
그런 다음, @auth
slot 내에 default.js
파일을 추가하여 모달이 활성 상태가 아닐 때 렌더링되지 않도록 합니다.
export default function Default() {
return '...'
}
export default function Default() {
return '...'
}
@auth
slot 내에서 /(.)login
폴더를 업데이트하여 /login
라우트를 가로챕니다. /(.)login/page.tsx
파일에 <Modal>
컴포넌트와 자식을 가져옵니다:
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'
export default function Page() {
return (
<Modal>
<Login />
</Modal>
)
}
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'
export default function Page() {
return (
<Modal>
<Login />
</Modal>
)
}
알아두면 좋은 점:
- 라우트를 가로채기 위해 사용하는 규칙, 예:
(.)
는 파일 시스템 구조에 따라 다릅니다. Intercepting Routes convention을 참조하세요.- 모달 콘텐츠 (
<Login>
)에서<Modal>
기능을 분리함으로써 모달 내의 모든 콘텐츠, 예: forms,가 서버 컴포넌트가 되도록 할 수 있습니다. 자세한 내용은 Interleaving Client and Server Components를 참조하세요.
모달 열기
이제 Next.js 라우터를 활용하여 모달을 열고 닫을 수 있습니다. 이를 통해 모달이 열릴 때와 이전 및 이후 탐색 시 URL이 올바르게 업데이트되도록 보장할 수 있습니다.
모달을 열려면, 부모 레이아웃에 @auth
slot을 prop으로 전달하고 children
prop과 함께 렌더링합니다.
import Link from 'next/link'
export default function Layout({
auth,
children,
}: {
auth: React.ReactNode
children: React.ReactNode
}) {
return (
<>
<nav>
<Link href="/login">Open modal</Link>
</nav>
<div>{auth}</div>
<div>{children}</div>
</>
)
}
import Link from 'next/link'
export default function Layout({ auth, children }) {
return (
<>
<nav>
<Link href="/login">Open modal</Link>
</nav>
<div>{auth}</div>
<div>{children}</div>
</>
)
}
사용자가 <Link>
를 클릭하면 /login
페이지로 이동하는 대신 모달이 열립니다. 그러나 새로 고침 또는 초기 로드 시 /login
으로 이동하면 주요 로그인 페이지로 이동합니다.
모달 닫기
모달을 닫으려면 router.back()
을 호출하거나 Link
컴포넌트를 사용할 수 있습니다.
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<>
<button
onClick={() => {
router.back()
}}
>
Close modal
</button>
<div>{children}</div>
</>
)
}
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }) {
const router = useRouter()
return (
<>
<button
onClick={() => {
router.back()
}}
>
Close modal
</button>
<div>{children}</div>
</>
)
}
Link
컴포넌트를 사용하여 더 이상 @auth
slot을 렌더링하지 않아야 하는 페이지로 이동할 때는 평행 라우트가 null
을 반환하는 컴포넌트와 일치하도록 해야 합니다. 예를 들어, 루트 페이지로 돌아갈 때 @auth/page.tsx
컴포넌트를 생성합니다:
import Link from 'next/link'
export function Modal({ children }: { children: React.ReactNode }) {
return (
<>
<Link href="/">Close modal</Link>
<div>{children}</div>
</>
)
}
import Link from 'next/link'
export function Modal({ children }) {
return (
<>
<Link href="/">Close modal</Link>
<div>{children}</div>
</>
)
}
export default function Page() {
return '...'
}
export default function Page() {
return '...'
}
또는 다른 페이지(예: /foo
, /foo/bar
등)로 이동할 때 catch-all slot을 사용할 수 있습니다:
export default function CatchAll() {
return '...'
}
export default function CatchAll() {
return '...'
}
알아두면 좋은 점:
- Active state and navigation에서 설명한 동작으로 인해 slot과 더 이상 일치하지 않는 라우트로 클라이언트 측 탐색 시 slot이 계속 표시되므로 모달을 닫기 위해 slot이
null
을 반환하는 라우트와 일치시켜야 합니다.- 다른 예로는 전용
/photo/[id]
페이지가 있는 갤러리에서 사진 모달을 열거나, 사이드 모달에서 쇼핑 카트를 여는 것이 포함될 수 있습니다.- Intercepted 및 Parallel Routes가 포함된 모달의 예제 보기 (opens in a new tab).
Loading and Error UI
Parallel Routes는 독립적으로 스트리밍될 수 있으므로 각 라우트에 대해 독립적인 오류 및 로딩 상태를 정의할 수 있습니다:
Loading UI 및 Error Handling 문서를 참조하여 자세한 정보를 확인하세요.