Parallel Routes
Parallel Routes는 동시에 또는 조건부로 동일한 레이아웃 내에서 하나 이상의 페이지를 렌더링할 수 있게 해준다. 이는 앱의 매우 동적인 섹션인 대시보드나 소셜 사이트의 피드와 같은 경우에 유용하다.
예를 들어, 대시보드를 고려해보면 병렬 루트를 사용하여 팀 및 분석 페이지를 동시에 렌더링할 수 있다.
Parallel Routes는 명명된 슬롯을 사용하여 생성됩니다. 슬롯은 @폴더 관례로 정의된다. 아래 예시는 @team 및 @user 두 개의 슬롯을 정의한다.
슬롯은 공유 부모 레이아웃에 속성(props)으로 전달된다. 위의 예에서는 app/layout.js의 컴포넌트가 이제 @team 및 @user 슬롯 속성을 받아들이고, 이를 자식 속성(children prop)과 함께 병렬로 렌더링할 수 있다.
export default function Layout({
children,
user,
team,
}: {
children: React.ReactNode
user: React.ReactNode
team: React.ReactNode
}) {
return (
<section>{children}{user}{team}</section>
)
}
슬롯은 루트 세그먼트가 아니며 URL 구조에 영향을 미치지 않는다.
default.js
초기 로드 또는 전체 페이지 다시 로드 중에 일치하지 않는 슬롯에 대한 대체 항목으로 렌더링할 파일을 정의할 수 있다.
위와 같은 경로가 있다고 가정했을 경우 @team 폴더에는 settings라는 폴더가 있지만 @user의 경우는 폴더가 없다. 그래서 '/dasboard/settings/'에 접근 시 default.js가 없을 시 404에러가 발생하게 되는데 default.js 파일을 만들어줌으로써 이를 방지할 수 있다.
조건부 경로
Parallel Routes는 특정 조건에 따라 슬롯을 조건부로 렌더링할 수 있도록 허용한다. 예를 들어 언어가 한국일때만 user page를 보여주고 다른 경우는 team page를 보고 싶을 시 다음과 같이 작성할 수 있다.
export default function Layout({
children,
user,
team,
params
}: {
children: React.ReactNode
user: React.ReactNode
team: React.ReactNode
params: {
lang: string
}
}) {
// default는 team page
let page = team
// lang이 ko면 user page
if(params.lang == 'ko'){
page = user
}
return (
<section>{children}{page}</section>
)
}
Streaming
Parallel Routes는 독립적으로 스트리밍될 수 있어 각 루트에 대해 독립적인 오류 및 로딩 상태를 정의할 수 있다.
구현 예시
'use client'
import {useState} from "react";
export default function Page(){
const [backgroundColor, setBackgroundColor] = useState('blue');
const handleColorChange = () => {
// 랜덤한 배경색을 생성하기 위한 함수
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
// 새로운 랜덤한 배경색으로 설정
const newColor = getRandomColor();
setBackgroundColor(newColor);
};
return (
<>
<div className="w-1/2">
<button className="w-full" onClick={handleColorChange}>change button</button>
<div className="w-full h-full">
<div style={{
backgroundColor: backgroundColor,
padding: '20px',
textAlign: 'center',
cursor: 'pointer',
}} className="w-full h-96">team page
</div>
</div>
</div>
</>
)
}
위와 같은 페이지를 @user와 @team에 만들고 layout에 다음과 같이 user와 team의 route를 추가한다.
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function Layout({
children,
user,
team,
params
}: {
children: React.ReactNode
user: React.ReactNode
team: React.ReactNode
params: {
lang: string
}
}) {
return (
<section>
{children}
<div className="flex">
{team}
{user}
</div>
</section>
)
}
그 다음 실행시키면
다음과 같은 페이지가 나오면 상단의 button 클릭시 각각의 route event가 발생하여 색상이 변하는것을 확인 할 수 있다.
Intercepting Routes
Intercepting Routes는 현재 레이아웃 내에서 애플리케이션의 다른 부분에서 루트를 로드할 수 있게 해준다. 이 라우팅 패러다임은 사용자가 다른 컨텍스트로 전환하지 않고 루트의 내용을 표시하고자 할 때 유용할 수 있다.
예를 들어, 피드에서 사진을 클릭할 때 해당 사진을 모달에서 표시할 수 있다. 이 경우 Next.js는 /photo/123 루트를 가로채고 URL을 마스킹하여 /feed 위에 오버레이한다.
그러나 공유 가능한 URL을 클릭하거나 페이지를 새로 고침하여 사진에 접근할 때에는 모달 대신 전체 사진 페이지가 렌더링되어야 한다. 이때는 라우트 가로채기가 발생하지 않아야 한다.
Intercepting Routes는 (..) 관례를 사용하여 정의할 수 있으며, 이는 상대 경로 관례 ../과 유사하지만 세그먼트를 위한 것이다.
다음과 같이 사용할 수 있다:
- (.) : 동일한 레벨의 세그먼트와 일치
- (..) : 하나 위 레벨의 세그먼트와 일치
- (..)(..) : 두 레벨 위의 세그먼트와 일치
- (...) : 루트 앱 디렉토리에서 세그먼트와 일치
예를 들어, 피드 세그먼트 내에서 (..)photo 디렉토리를 생성하여 피드 내부에서 사진 세그먼트를 가로챌 수 있다.
Modals
Intercepting Routes를 Parallel Routes와 함께 사용하여 모달을 만들 수 있다.
이 패턴을 사용하여 모달을 만들면 모달과 관련된 몇 가지 일반적인 문제를 극복할 수 있다. 다음과 같은 기능이 가능해진다.
- 모달 콘텐츠를 URL을 통해 공유 가능하게 함
- 페이지를 새로 고침할 때 모달을 닫는 대신 컨텍스트를 보존
- 이전 라우트로 이동하는 대신 모달을 닫음
- 앞으로 이동하는 경우 모달을 다시 열 수 있음
구현 예시
//@modal/(.)photos/[id]/madal.tsx
'use client'
import React, {ElementRef, useEffect, useRef} from "react";
import {useRouter} from "next/navigation";
import {createPortal} from "react-dom";
export function Modal({children} : {children: React.ReactNode}){
const router = useRouter();
const dialogRef = useRef<ElementRef<'dialog'>>(null);
useEffect(() => {
if (!dialogRef.current?.open) {
dialogRef.current?.showModal();
}
}, []);
function onDismiss(){
router.back()
}
return createPortal(
<div className="modal-backdrop">
<dialog ref={dialogRef} className="modal" onClose={onDismiss}>
{children}
<button onClick={onDismiss} className="close-button" />
</dialog>
</div>,
document.getElementById('modal-root')!
);
}
//@modal/(.)photos/[id]/page.tsx
import {Modal} from "@/app/[lang]/@modal/(.)photos/[id]/modal";
export default function PhotoModal({
params : { id: photoId},
}:{
params : { id: string };
}) {
return <Modal>{photoId}</Modal>
}
//photos/[id]/page.tsx
import {Modal} from "@/app/[lang]/@modal/(.)photos/[id]/modal";
export default function PhotoModal({
params : { id: photoId},
}:{
params : { id: string };
}) {
return <Modal>{photoId}</Modal>
}
다음과 같이 Intercepting Routes를 구성하고 layout에 다음과 같이 Parallel Routes를 구성한다.
<section>
{children}
{modal}
<div id="modal-root"/>
</section>
그후 실행하면 다음과 같이 나온다.
위의 화면에서 button을 클릭하면 /ko/photos/id로 요청을 하게 되는데 phothos/[id]/page.tsx 로 가는 요청을 @modal에서 Intercept하여 동일한 화면에서 모달을 띄울 수 가 있다.
또한, 버튼을 통해서가 아닌 직접 /ko/photos/id로 접근시에는 phothos/[id]/page.tsx가 있으므로 에러가 발생되지 않는다.
'Next.js 개발 가이드 > 02. 코딩 가이드 및 필수 패키지' 카테고리의 다른 글
9. Meta Data (0) | 2024.02.01 |
---|---|
8. Error Handling (1) | 2024.02.01 |
6. Middleware (0) | 2024.01.26 |
5. Internationalization (0) | 2024.01.24 |
4. loading, Suspense, error boundary, zod, dynamic routes, searchParams 사용 예시 (0) | 2024.01.21 |