일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- tanstack query
- 리액트 공식문서
- 코딩테스트
- 자바스크립트
- REACT
- useState
- 파이썬 반복문
- 내배캠 프로젝트
- 타입스크립트
- 파이썬 replace
- 한글 공부 사이트
- 리액트 훅
- typeScript
- 타입스크립트 리액트
- 리액트 프로젝트
- 내일배움캠프
- 파이썬 딕셔너리
- 내일배움캠프 최종 프로젝트
- 파이썬 for in
- useEffect
- 파이썬 slice
- Next 팀 프로젝트
- 내일배움캠프 프로젝트
- 리액트
- React Hooks
- 프로그래머스
- 리액트 공식 문서
- 파이썬 for
- JavaScript
- 파이썬 enumerate
- Today
- Total
sohyeon kim
[Next] Next.js 공식문서 정리 1 : 라우팅 Routing 개념, 컨벤션, 알아두면 좋은 점 본문
💡 Next.js 공식문서 정리 - 라우팅 Routing
1. 시작하기
Next.js란?
- 풀스택 웹 애플리케이션을 구축하기 위한 React 프레임워크
- 사용자 인터페이스를 만들기 위해 React 컴포넌트를 사용하고, 추가 기능 & 최적화를 위해 Next.js 를 사용한다.
- 내부적으로 Next.js 는 번들링, 컴파일링 등 React 에 필요한 도구를 추상화하고 자동으로 구성한다.
➡️ 이를 통해 설정에 시간들이지 않고 애플리케이션 구축에 집중할 수 있다.
Next.js 의 주요 기능
- 라우팅 : 레이아웃, 중첩 라우팅, 로딩 상태, 오류 처리 등을 지원하는 서버 컴포넌트 기반의 파일 시스템 라우터
- 렌더링 : 클라이언트 및 서버 컴포넌트를 사용한 렌더링, 정적 및 동적 렌더링으로 서버에서 추가 최적화, Edge 및 Node.js 런타임에서 스트리밍
- 데이터 가져오기 : 서버 컴포넌트에서 async/await 을 사용한 간단한 데이터 가져오기와 요청 메모이제이션, 데이터 캐싱 및 재검증을 위한 확장된 fetch API
- 스타일링 : CSS 모듈, Tailwind CSS, CSS-in-JS 를 포함한 스타일링 방법 지원
- 최적화 : 애플리케이션의 Core Web Vitals 와 사용자 경험을 향상시키기 위한 이미지, 폰트, 스크립트 최적화
- TypeScript : 향상된 타입 검사 및 더 효율적인 컴파일, 맞춤형 TS 플러그인과 타입 검사기를 통한 TS 지원 개선
App Router vs Pages Router
- 앱 라우터는 서버 컴포넌트와 스트리밍과 같은 React 의 최신 기능을 사용할 수 있는 새로운 라우터이다.
- 페이지 라우터는 서버 렌더링된 React 앱을 구축할 수 있게 해주는 원래의 Next.js 라우터로, 이전 앱을 위해 계속 지원된다.
아래 부터 App Router 기준으로 작성
프로젝트 구조
최상위 폴더
app | 앱 라우터 |
public | 정적 자산을 제공 |
src | 선택적 애플리케이션 소스 폴더 |
최상위 파일 : 애플리케이션을 구성, 종속성 관리, 미들웨어 실행, 모니터링 도구 통합, 환경 변수를 정의하는 데 사용
next.config.js | Next.js 구성 파일 |
package.json | 프로젝트 종속성과 스크립트 |
instrumentation.ts | OpenTelemetry 및 계측 파일 |
middleware.ts | Next.js 요청 미들웨어 |
.env | 환경 변수 |
.env.local | 로컬 환경 변수 |
.env.production | 프로덕션 환경 변수 |
.env.development | 개발 환경 변수 |
.eslintrc.json | ESLint 구성 파일 |
.gitignore | Git에서 무시할 파일 및 폴더 |
next-env.d.ts | Next.js를 위한 TypeScript 선언 파일 |
tsconfig.json | TypeScript 구성 파일 |
jsconfig.json | JavaScript 구성 파일 |
app 라우팅 관례(파일 규칙) : 앱 라우트에서 라우트를 정의하고 메타데이터를 처리하는 데 사용하는 특수 파일
layout | .js .jsx .tsx | 세그먼트와 자식에 대한 공유 레이아웃 UI |
page | .js .jsx .tsx | 경로의 고유한 UI 로 경로를 공개적으로 접근 가능하게 만든다. |
loading | .js .jsx .tsx | 세그먼트와 자식에 대한 로딩 UI |
not-fount | .js .jsx .tsx | 세그먼트와 자식에 대한 찾을 수 없는 UI |
error | .js .jsx .tsx | 세그먼트와 자식에 대한 오류 UI |
global-error | .js .jsx .tsx | 전역 오류 UI |
route | .js .ts | 서버 사이드 API 엔드포인트 |
template | .js .jsx .tsx | 특수한 재렌더링된 레이아웃 |
default | .js .jsx .tsx | 병렬 라우트 대체(폴백) UI |
중첩 라우트
folder | 라우트 세그먼트 |
folder/folder | 중첩 라우트 세그먼트 |
동적 라우트
[folder] | 동적 라우트 세그먼트 |
[...folder] | 모든 것을 포함하는 라우트 세그먼트 |
[[...folder]] | 선택적 모든 것을 포함하는 라우트 |
라우트 그룹 및 비공개 폴더
(folder) | 라우팅에 영향을 주지 않고 그룹화 |
_folder | 라우팅에서 폴더 및 모든 자식 세그먼트 제외 |
폴더와 파일의 역할
- 폴더 : 경로를 정의하는 데 사용.
경로는 루트 폴더에서 시작해 page.js 를 포함하는 최종 리프 폴더까지 이르는 중첩 폴더의 단일 경로이다. - 파일 : 경로 세그먼트에 대해 표시되는 UI 를 생성하는 데 사용.
2. 레이아웃 및 템플릿 Layouts & Templates
특수 파일인 layout.js 와 template.js 는 경로 간에 공유되는 UI 를 생성할 수 있게 해준다.
레이아웃
- 탐색 시 레이아웃은 상태와 상호작용을 유지하며, 다시 렌더링되지 않는다.
- 레이아웃은 중첩될 수 있다.
- 렌더링 중 자식 레이아웃(존재하는 경우) 또는 페이지로 채워질 children prop 을 받아야 한다.
- 레이아웃은 기본적으로 서버 컴포넌트이지만, 클라이언트 컴포넌트로 설정 가능하다.
- 레이아웃은 데이터를 가져올 수 있다.
- 부모 레이아웃과 자식 간의 데이터 전달은 불가능하다. 그러나 경로에서 동일한 데이터를 여러 번 가져올 수 있으며,
React 는 성능에 영향을 주지 않고 중복 요청을 자동으로 제거한다. - 레이아웃은 pathname 에 접근 할 수 없다. 하지만 클라이언트 컴포넌트라면 usePathname 을 사용해 접근할 수 있다.
- 경로 그룹을 사용해 특정 경로 세그먼트를 공유 레이아웃에서 선택적으로 포함하거나 제외할 수 있다.
루트 레이아웃 *필수
- app 디렉토리의 최상위 레벨에서 정의되며 모든 경로에 정의된다.
- 이 레이아웃은 필수이며, 서버에서 반환되는 초기 HTML 을 수정할 수 있도록 html 및 body 태그를 포함해야 한다.
- 경로 그룹을 사용해 여러 개의 루트 레이아웃을 만들 수 있다.
레이아웃 중첩
- 기본적으로 폴더 계층 구조의 레이아웃은 중첩된다. 이는 children prop 을 통해 자식 레이아웃을 래핑함을 의미한다.
- 특정 경로 세그먼트(폴더) 안에 layout.js 를 추가해 레이아웃을 중첩할 수 있다.
템플릿
- 템플릿은 자식 레이아웃 또는 페이지를 래핑한다는 점에서 레이아웃과 유사하다.
- 경로 간 지속되고 상태를 유지하는 레이아웃과 달리, 템플릿은 탐색 시 자식의 새 인스턴스를 생성한다.
이는 사용자가 템플릿을 공유하는 경로 간에 탐색할 때마다 자식의 새 인스턴스가 마운트되고, DOM 요소가 재생성되며,
클라이언트 컴포넌트의 상태가 유지되지 않고, 이펙트가 다시 동기화됨을 의미한다.
이러한 특정 동작이 필요한 경우 템플릿이 레이아웃보다 더 적합한 옵션일 수 있다.- 탐색 시 useEffect 를 다시 동기화하려는 경우
- 탐색 새 자식 클라이언트 컴포넌트의 상태를 재설정하려는 경우
- 템플릿 컴포넌트는 children prop 을 받아야 한다.
- 중첩 측면에서 template.js 는 레이아웃과 자식 사이 렌더링 된다. 다음은 간략화된 코드이다.
<Layout>
{/* 템플릿은 고유한 키를 부여받습니다. */}
<Template key={routeParam}>{children}</Template>
</Layout>
메타데이터
- title 및 meta 와 같은 <head> HTML 요소를 메타데이터 API 를 사용해 수정할 수 있다.
- 메타데이터는 metadata 객체 또는 generateMetadata 함수를 layout.js 또는 page.js 파일에서 export 해 정의할 수 있다.
- 루트 레이아웃에 title 및 meta <head> 태그를 수동으로 추가하지 말아야 한다.
대신 메타데이터 API 를 사용해 스트리밍 및 <head> 요소 중복 제거와 같은 고급 요구사항을 자동으로 처리하자.
활성 탐색 링크
- usePathname() 훅을 사용해 탐색 링크가 활성 상태인지 확인할 수 있다.
- usePathname() 은 클라이언트 훅이기에 탐색 링크를 클라이언트 컴포넌트로 추출한 후 레이아웃 또는 템플릿에 import 해야 한다.
3. 경로 간 이동 Linking & Navigating
Next.js 의 경로 간 이동 방법 네 가지
- <Link> 컴포넌트 사용
- useRouter 훅 사용 (클라이언트 컴포넌트)
- redirect 함수 사용 (서버 컴포넌트)
- 네이티브 History API 사용
<Link> 컴포넌트
- HTML <a> 태그를 확장해 경로 간 사전 로드와 클라이언트 측 내비게이션을 제공하는 내장 컴포넌트
- Next.js 에서 기본적으로 권작되는 방법
동적 세그먼트에 링크하기 Linking to Dynamic Sagments
- 템플릿 리터럴과 인터폴레이션을 사용해 링크 목록 생성 가능
e.g. 블로그 게시물 목록 생성
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
활성 링크 확인하기 Checking Active Links
- usePathname() 을 사용해 링크가 활성화되었는지 확인 가능
e.g. 현재 pathname 이 링크의 href 와 일치하는지 확인해 활성 링크에 클래스를 추가할 수 있다.
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Links() {
const pathname = usePathname()
return (
<nav>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
<Link
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/about"
>
About
</Link>
</nav>
)
}
id 로 스크롤하기 Scrolling to an id
- Next.js 앱 라우터의 기본 동작은 새 경로로 이동할 때 맨 위로 스크롤하거나 이전 및 다음 탐색을 위해 스크롤 위치를 유지하는 것
- 내비게이션 시 특정 id 로 스크롤하려면 URL 에 # 해시 링크를 추가하거나 href prop 에 해시 링크를 전달할 수 있다.
- <Link> 는 <a> 요소로 렌더링 되기에 가능하다.
<Link href="/dashboard#settings">Settings</Link>
<!-- 출력 -->
<a href="/dashboard#settings">Settings</a>
스크롤 복원 비활성화 Disabling scroll restoration
- 스크롤의 기본 규칙(위 내용)을 비활성화하려면 <Link> 에 scroll={false} 를 전달하거나
router.push() 또는 router.replace() 에 scroll: false 를 전달할 수 있다.
<!-- next/link -->
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>
<!-- useRouter -->
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard', { scroll: false })
useRouter() hook
- useRouter 훅을 사용해 클라이언트 컴포넌트에서 프로그래밍 방식으로 경로를 변경할 수 있다.
- 특정 요구 사항이 없는 한(프로그래밍 방식이 필요없다면) 경로 간 내비게이션을 위해 <Link> 를 사용하는 것을 권장한다.
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
redirect function
- 서버 컴포넌트의 경우 redirect 함수를 사용하자.
- redirect 는 기본적으로 307(임시 리다이렉션) 상태 코드를 반환한다. 서버 액션에서 사용될 때는 303(다른 페이지 보기)를 반환하며, 이는 POST 요청 결과로 성공 페이지로 리다이렉션하는 데 자주 사용된다.
- redirect 는 내부적으로 오류를 발생시키므로 try/catch 블록 외부에서 호출해야 한다.
- redirect 는 렌더링 과정 중 클라이언트 컴포넌트에서 호출될 수 있지만, 이벤트 핸들러에선 호출될 수 없다. 대신 useRouter 훅 사용
import { redirect } from 'next/navigation'
async function fetchTeam(id: string) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }: { params: { id: string } }) {
const team = await fetchTeam(params.id)
if (!team) {
redirect('/login')
}
// ...
}
네이티브 History API 사용 Using the native History API
- Next.js 는 네이티브 window.history.pushState 및 window.history.replaceState 메서드를 사용해 페이지를 다시 로드하지 않고 브라우저의 히스토리 스택을 업데이트할 수 있다.
- pushState 및 replaceState 호출은 Next.js 라우터와 통합되어 usePathname 및 useSearchParame 와 동기화할 수 있다.
window.history.pushState
- 브라우저의 히스토리 스택에 새 항목을 추가하는 데 사용
- 사용자는 이전 상태로 돌아갈 수 있다. 예시로, 제품 목록을 정렬하려면 다음과 같이 한다.
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const params = new URLSearchParams(searchParams.toString())
params.set('sort', sortOrder)
window.history.pushState(null, '', `?${params.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Sort Ascending</button>
<button onClick={() => updateSorting('desc')}>Sort Descending</button>
</>
)
}
window.history.replaceState
- 브라우저의 히스토리 스택에서 현재 항목을 대체하는 데 사용
- 사용자는 이전 상태로 돌아갈 수 없다. 예시로, 애플리케이션의 로케일을 변경하려면 다음과 같이 한다.
'use client'
import { usePathname } from 'next/navigation'
export function LocaleSwitcher() {
const pathname = usePathname()
function switchLocale(locale: string) {
// 예: '/en/about' 또는 '/fr/contact'
const newPath = `/${locale}${pathname}`
window.history.replaceState(null, '', newPath)
}
return (
<>
<button onClick={() => switchLocale('en')}>English</button>
<button onClick={() => switchLocale('fr')}>French</button>
</>
)
}
라우팅 및 네비게이션 작동 방식 How Routing & Navigation Works
- 앱 라우터는 라우팅 및 내비게이션에 대해 하이브리드 접근 방식을 사용한다.
- 서버에서 애플리케이션 코드는 경로 세그먼트에 의해 자동으로 코드 분할되며,
클라이언트에서 Next.js 는 경로 세그먼트를 사전 로드하고 캐시한다. - 즉, 사용자가 새 경로로 내비게이션할 때 브라우저가 페이지를 다시 로드하지 않으며, 변경된 경로 세그먼트만 다시 렌더링된다.
이는 내비게이션 경험과 성능을 향상시킨다.
1. 코드 분할 Code Splitting
- 코드분할은 애플리케이션 코드를 더 작은 번들로 나눠 브라우저가 다운로드하고 실행할 수 있게 한다.
이는 전송되는 데이터 양과 각 요청에 대한 실행 시간을 줄여 성능을 향상시킨다. - 서버 컴포넌트를 사용하면 애플리케이션 코드가 경로 세그먼트에 의해 자동으로 코드 분할되는데,
이는 현재 경로에 필요한 코드만 내비게이션 시 로드된다는 것을 의미한다.
2. 사전 로드 Prefetching
- 사전 로드는 사용자가 방문하기 전 백그라운드에서 경로를 미리 로드하는 방법이다.
Next.js 에서 경로를 사전 로드하는 두 가지 방법- <Link> 컴포넌트 : 경로가 사용자의 뷰포트에 보이게 되면 자동으로 사전 로드. 사전 로드는 페이지가 처음 로드될 때 또는 스크롤을 통해 뷰에 들어올 때 발생한다.
- router.prefetch() : useRouter 훅을 사용해 프로그래밍 방식으로 경로를 사전 로드 할 수 있다.
- 사전 로드는 개발 환경이 아닌 프로덕션 환경에서만 활성화된다.
3. 캐싱 Caching
- Next.js 에는 라우터 캐시라는 메모리 내 클라이언트 측 캐시가 있다.
사용자가 애플리케이션을 탐색 할 때 사전 로드된 경로 세그먼트와 방문한 경로의 React 서버 컴포넌트 페이로드가 캐시에 저장된다.이는 내비게이션 시 새 서버 요청을 만드는 대신 가능한 한 캐시를 재사용해 요청 수와 전송되는 데이터 양을 줄여 성능을 향상시킨다.
4. 부분 렌더링 Partial Rendering
- 부분 렌더링은 내비게이션 시 변경된 경로 세그먼트만 클라이언트에서 다시 렌더링되고, 공유된 세그먼트는 유지되는 것을 말한다.
- 예시로, 두 형제 경로인 /dashboard/settings 와 /dashboard/analytics 간 내비게이션할 때 settings 와 analytics 페이지가 렌더링되고, 공유된 dashboard 레이아웃은 유지된다.
- 부분 렌더링이 없다면, 각 내비게이션은 전체 페이지를 클라이언트에서 다시 렌더링하게 된다.
- 변경된 세그먼트만 렌더링하면 전송되는 데이터 양과 실행 시간을 줄여 성능을 향상시킬 수 있다.
5. 소프트 내비게이션 Soft Navigation
- 브라우저는 페이지 간 내비게이션 시 '하드 내비게이션'을 수행한다.
- 앱 라우터는 페이지 간 '소프트 내비게이션'을 가능하게 해 변경된 경로 세그먼트만 리렌더링되도록 한다. (부분 렌더링)
- 이를 통해 내비게이션 중 클라이언트 React 상태가 유지된다.
6. 뒤로 및 앞으로 내비게이션 Back & Forward Navigation
- 기본적으로 Next.js 뒤로 맟 앞으로 내비게이션을 위해 스크롤 위치를 유지하고 라우터 캐시의 경로 세그먼트를 재사용한다.
4. 에러 핸들링 Error Handling
에러는 예상된 에러와 예상치 못한 예외 두 가지로 나눌 수 있다.
- 예상된 에러를 반환 값으로 모델링 : 서버 액션에서 예상된 에러를 try/catch 로 처리하는 것을 피하자. useActionState 를 사용해 이러한 에러를 관리하고 클라이언트에 반환하자.
- 예상치 못한 에러는 에러 경계로 처리 : error.tsx 및 global-error.tsx 파일을 통해 에러 경계를 구현하고 예상치 못한 에러를 처리하며 대체 UI 를 제공하자.
예상된 에러 핸들링 Handling Expected Errors
- 서버 측 폼 검증이나 실패한 요청 등 정상적인 애플리케이션 운영 중 발생할 수 있는 에러
- 이러한 에러는 명시적으로 처리하고 클라이언트에 반환해야 한다.
서버 액션의 예상된 에러 핸들링 Handling Expected Errors from Server Actions
- useActionState 훅(이전에는 useFormState)을 사용해 서버 액션의 상태를 관리하고 에러를 처리한다.
- 이 접근 방식은 예상된 에러를 위한 try/catch 를 피하고, 에러를 throw 하는 대신 반환 값으로 모델링해야 한다.
- 그 다음 useActionState 훅에 액션을 전달하고 반환된 state 를 사용해 에러 메시지를 표시할 수 있다.
- 클라이언트 컴포넌트에서 반환된 상태를 사용해 토스트 메시지를 표시할 수도 있다.
'use server'
import { redirect } from 'next/navigation'
export async function createUser(prevState: any, formData: FormData) {
const res = await fetch('https://...')
const json = await res.json()
if (!res.ok) {
return { message: 'Please enter a valid email' }
}
redirect('/dashboard')
}
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button>Sign up</button>
</form>
)
}
서버 컴포넌트의 예상된 에러 핸들링 Handling Expected Erros from Server Components
- 서버 컴포넌트 내에서 데이터를 가져올 때는 응답을 사용해 조건부로 에러 메시지를 렌더링하거나 redirect 할 수 있다.
export default async function Page() {
const res = await fetch(`https://...`)
const data = await res.json()
if (!res.ok) {
return 'There was an error.'
}
return '...'
}
예상치 못한 예외(에러) Uncaught Exceptions
- 예상치 못한 예외는 정상적인 애플리케이션 흐름 중 발생해서는 안되는 버그나 문제를 나타내는 에러이다. (예외 에러라 하겠다.)
- 이러한 에러는 throw 하여 에러 경계에서 처리해야 한다.
- 일반적인 경우 : 루트 레이아웃 아래에서 발생한 예외 에러는 error.js 로 처리
- 선택적인 경우 : 중첩된 error.js 파일(e.g. app/dashboard/error.js)을 사용해 세분화된 예외 에러를 처리
- 드문 경우 : 루트 레이아웃에서 발생한 예외 에러는 global-error.js 로 처리
에러 경계 사용하기 Using Error BOundaries
- Next.js 는 예외 에러를 처리하기 위해 에러 경계를 사용한다.
- 에러 경계는 자식 컴포넌트에서 발생한 에러를 캐치하고, 충돌한 컴포넌트 트리 대신 대체 UI 를 표시한다.
- 경로 세그먼트 내에 error.tsx 파일을 추가하고 React 컴포넌트를 내보내 에러 경계를 생성하자.
'use client' // Error boundaries must be Client Components
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
)
}
가장 가까운 부모에서 에러 핸들링 Handling Erros in Nested Routes
- 에러는 가장 가까운 부모 에러 경계로 전파된다.
- 이를 통해 경로 계층 구조의 다양한 수준에서 error.tsx 파일을 배치해 세분화된 에러 처리가 가능하다.
전역 에러 핸들링 Handling Global Errors
- 드물게 루트 레이아웃에서 에러를 처리해야 할 땐, app/global-error.js 를 사용한다.
- 이 파일은 루트 앱 디렉토리에 위치하며, 국제화를 활용할 때도 작동한다.
- 전역 에러 UI 는 활성화될 때 루트 레이아웃이나 템플릿을 대체하므로 자체 <html> 및 <body> 태그를 정의해야 한다.
'use client' // Error boundaries must be Client Components
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
// global-error must include html and body tags
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
5. 로딩 UI 및 스트리밍 Loading & Streaming
특정 경로 세그먼트에 대한 폴백을 생성하고, 콘텐츠가 준비되는 대로 자동으로 스트리밍할 수 있게 하는 Suspense 기반의 로딩 UI 를 사용하라.
즉시 로딩 상태 Instant Loading States
- loading.js 파일을 추가해 폴더 내에 로딩 상태를 생성한다.
Suspense 를 사용한 스트리밍 Streaming with Suspense
- Suspense 경계는 UI 컴포넌트를 수동으로 스트리밍할 수 있도록 한다.
- Node.js 및 Edge 런타임에서도 사용 가능하다.
스트리밍이란?
- SSR 의 단점을 해결하기 위해 페이지의 HTML 을 더 작은 청크로 나누고 서버에서 클라이언트로 점진적으로 전송하는 방식
- 더 많은 Suspense 예제와 사용 사례
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
검색 엔진 최적화SEO
- Next.js 는 UI 를 스트리밍하기 전에 generateMetadata 내부에서 데이터 페칭이 완료될 때까지 대기한다.
- 이는 스트리밍 응답의 첫 번째 부분에 <head> 태그가 포함되도록 보장한다.
상태 코드 Status Codes
- 스트리밍 시 200 상태 코드를 반환해 요청이 성공했음을 알린다.
- redirect 또는 notFound 를 사용할 때와 같이 스트리밍된 콘텐츠 내에서 오류를 클라이언트로 전달할 수 있다.
- 응답 헤더는 이미 클라이언트에 전송되었기 때문에 응답의 상태 코드를 업데이트할 수 는 없다.
- 이는 SEO 에 영향을 미치지 않는다.
6. 리다이렉팅 Redirecting(리다이렉션 Redirection)
Next.js 에서 리다이렉션을 처리하는 방법. 각 옵션, 사용 사례 및 대규모 리다이렉션을 관리하는 방법을 설명한다.
API | Purpose | Where | Status Code |
redirect | 사용자 동작 또는 이벤트 후 리디렉션 | 서버 컴포넌트, 서버 액션, 라우트 핸들러 | 307 (Temporary) or 303 (Server Action) |
permanentRedirect | 사용자 동작 또는 이벤트 후 영구 리디렉션 | 서버 컴포넌트, 서버 액션, 라우트 핸들러 | 308 (Permanent) |
useRouter | 클라이언트 측 내비게이션 수행 | 클라이언트 컴포넌트의 이벤트 핸들러 | N/A |
redirects in next.config.js | 경로 기반으로 들어오는 요청 리디렉션 | next.config.js 파일 | 307 (Temporary) or 308 (Permanent) |
NextResponse.redirect | 조건 기반으로 들어오는 요청 리디렉션 | 미들웨어 | Any |
redirect function
- redirect 함수는 사용자를 다른 URL 로 리다이렉션할 수 있게 해준다.
- 서버 컴포넌트, 서버 액션, 라우트 핸들러에서 redirect 를 호출할 수 있다.
- redirect 는 종종 변형이나 이벤트 후 사용된다. 예시로, 게시물을 작성하는 경우👇
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function createPost(id: string) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidatePath('/posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
permanentRedirect funcion
- permanentRedirect 함수는 사용자를 다른 URL 로 영구적으로 리다이렉션할 수 있게 해준다.
- 서버 컴포넌트, 서버 액션, 라우트 핸들러에서 permanentRedirect 를 호출할 수 있다.
- permanentRedirect 는 종종 엔터티의 캐노니컬 URL 이 변경된 후 사용된다.
예시로, 사용자가 사용자 이름을 변경한 후 프로필 URL 을 업데이트 하는 경우👇
'use server'
import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function updateUsername(username: string, formData: FormData) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidateTag('username') // Update all references to the username
permanentRedirect(`/profile/${username}`) // Navigate to the new user profile
}
- permanentRedirect 는 기본적으로 308(영구 리다이렉션) 상태 코드를 반환한다.
- permanentRedirect 는 절대 URL 도 허용하며 외부 링크로 리다이렉션하는 데 사용할 수 있다.
- 렌더링 프로세스 전에 리다이렉션 하려면 next.config.js 또는 미들웨어를 사용하자.
useRouter() hook
- 클라이언트 컴포넌트의 이벤트 핸들러 내에서 리다이렉션이 필요하다면, useRouter 훅의 push 메서드를 사용할 수 있다.
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
redirects in next.config.js
- next.config.js 파일의 redirects 옵션을 사용하면, 들어오는 요청 경로를 다른 목적지 경로로 리다이렉션할 수 있다.
- 이는 페이지의 URL 구조를 변경하거나 미리 알려진 리다이렉션 목록이 있는 경우 유용하다.
- redirects 는 경로, 헤더, 쿠키 및 쿼리 매칭을 지원하여, 들어오는 요청을 기반으로 사용자를 리다이렉션할 수 있는 유연성을 제공
- redirects 를 사용하려면 next.config.js 파일에 옵션을 추가하라. 👇
- redirects 는 permanent 옵션과 함께 307 또는 308 상태 코드를 반환할 수 있다.
- redirects 는 플랫폼에 제한이 있을 수 있다. 예시로, Vercel 에서는 1,024개의 리다이렉션 제한이 있다. 대규모 리다이렉션(1000+)을 관리하려면 미들웨어를 사용해 사용자 지정 솔루션을 만드는 것을 고려하자.
- redirects는 미들웨어 이전에 실행된다.
module.exports = {
async redirects() {
return [
// Basic redirect
{
source: '/about',
destination: '/',
permanent: true,
},
// Wildcard path matching
{
source: '/blog/:slug',
destination: '/news/:slug',
permanent: true,
},
]
},
}
NextResponse.redirect in Middleware
- 미들웨어는 요청이 완료되기 전에 코드를 실행할 수 있게 한다.
- 그런 다음 들어오는 요청을 기반으로 다른 URL 로 리다이렉션할 수 있다.
- 이는 조건(e.g. 인증, 세션 관리 등)에 따라 사용자를 리다이렉션하거나 대규모 리다이렉션이 있는 경우 유용하다.
예시로 사용자가 인증되지 않은 경우 /login 페이지로 리다이렉션하려는 경우👇 - 미들웨어는 next.config.js 의 redirects 이후 및 렌더링 이전에 실행된다.
import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'
export function middleware(request: NextRequest) {
const isAuthenticated = authenticate(request)
// If the user is authenticated, continue as normal
if (isAuthenticated) {
return NextResponse.next()
}
// Redirect to login page if not authenticated
return NextResponse.redirect(new URL('/login', request.url))
}
export const config = {
matcher: '/dashboard/:path*',
}
대규모 리다이렉션 관리 Managing redirects at scale
- 대규모 리다이렉션(1000+)을 관리하려면 미들웨어를 사용해 사용자 지정 솔루션을 만드는 방법이 있다.
- 이를 통해 애플리케이션을 다시 배포하지 않고도 프로그램적으로 리다이렉션을 처리할 수 있다.
- 이를 위한 고려 사항은 아래와 같다.
1. 리다이렉션 맵 생성 및 저장 Creating & storing a redirect map
- 리다이렉션 맵은 데이터베이스(보통 키-값 저장소) 또는 JSON 파일에 저장할 수 있는 리다이렉션 목록이다.
- 미들웨어에서 Vercel 의 Edfe Config 또는 Redis 와 같은 DB에서 읽고, 들어오는 요청을 기반으로 사용자를 리다이렉션 할 수 있다.
{
"/old": {
"destination": "/new",
"permanent": true
},
}
import { NextResponse, NextRequest } from 'next/server'
import { get } from '@vercel/edge-config'
type RedirectEntry = {
destination: string
permanent: boolean
}
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
const redirectData = await get(pathname)
if (redirectData && typeof redirectData === 'string') {
const redirectEntry: RedirectEntry = JSON.parse(redirectData)
const statusCode = redirectEntry.permanent ? 308 : 307
return NextResponse.redirect(redirectEntry.destination, statusCode)
}
// No redirect found, continue without redirecting
return NextResponse.next()
}
2. 데이터 조회 성능 최적화 Optimizing data lookup performance
- 들어오는 요청마다 대규모 데이터셋을 읽는 것은 느리고 비용이 많이 들 수 있다.
- 데이터 조회 성능을 최적화할 수 있는 두 가지 방법이 있다.
- Verel Edge Config 또는 Redis 와 같이 빠른 읽기를 위해 최적화된 DB 를 사용한다.
- Bloom 필터와 같은 데이터 조회 전략을 사용해 더 큰 리다이렉션 파일이나 DB 를 읽기 전에 리다이렉션이 존재하는지 효율적으로 확인한다.
- 이전 예제를 고려해 생성된 bloom 필터 파일을 미들웨어에 가져온 다음, 들어오는 요청 경로가 필터에 존재하는지 확인할 수 있다.
- 만약 그렇다면, 요청을 라우터 핸들러로 전달해 실제 파일을 확인하고, 사용자를 적절한 URL 로 리다이렉션 한다.
- 이는 대규모 리다이렉션 파일을 미들웨어에 가져오는 것을 방지해 들어오는 모든 요청을 느리게 만들 수 있다.
- bloom-filters 와 같은 라이브러리를 사용해 bloom 필터를 생성할 수 있다.
- 라우트 핸들러에 대한 요청을 유효성 검사해 악의적인 요청을 방지해야 한다.
import { NextResponse, NextRequest } from 'next/server'
import { ScalableBloomFilter } from 'bloom-filters'
import GeneratedBloomFilter from './redirects/bloom-filter.json'
type RedirectEntry = {
destination: string
permanent: boolean
}
// Initialize bloom filter from a generated JSON file
const bloomFilter = ScalableBloomFilter.fromJSON(GeneratedBloomFilter as any)
export async function middleware(request: NextRequest) {
// Get the path for the incoming request
const pathname = request.nextUrl.pathname
// Check if the path is in the bloom filter
if (bloomFilter.has(pathname)) {
// Forward the pathname to the Route Handler
const api = new URL(
`/api/redirects?pathname=${encodeURIComponent(request.nextUrl.pathname)}`,
request.nextUrl.origin,
)
try {
// Fetch redirect data from the Route Handler
const redirectData = await fetch(api)
if (redirectData.ok) {
const redirectEntry: RedirectEntry | undefined =
await redirectData.json()
if (redirectEntry) {
// Determine the status code
const statusCode = redirectEntry.permanent ? 308 : 307
// Redirect to the destination
return NextResponse.redirect(redirectEntry.destination, statusCode)
}
}
} catch (error) {
console.error(error)
}
}
// No redirect found, continue the request without redirecting
return NextResponse.next()
}
import { NextRequest, NextResponse } from 'next/server'
import redirects from '@/app/redirects/redirects.json'
type RedirectEntry = {
destination: string
permanent: boolean
}
export function GET(request: NextRequest) {
const pathname = request.nextUrl.searchParams.get('pathname')
if (!pathname) {
return new Response('Bad Request', { status: 400 })
}
// Get the redirect entry from the redirects.json file
const redirect = (redirects as Record<string, RedirectEntry>)[pathname]
// Account for bloom filter false positives
if (!redirect) {
return new Response('No redirect', { status: 400 })
}
// Return the redirect entry
return NextResponse.json(redirect)
}
7. 라우트 그룹 Route Groups (folderName)
app 디렉토리에 중첩된 폴더는 일반적으로 URL 경로에 매핑되지만, 폴더를 라우터 그룹으로 표시해 해당 폴더가 URL 경로에 포함되지 않도록 할 수 있다. 이를 통해 경로 세그먼트와 프로젝트 파일을 논리적인 그룹으로 구성하면서 URL 경로 구조에 영향을 주지 않을 수 있다.
Route Groups 는 다음과 같은 경우 유용하다.
- URL 경로에 영향을 주지 않고 경로를 그룹화. (e.g. 사이트 섹션, 팀별로 그룹화)
- 동일한 경로 세그먼트 레벨에서 중첩 레이아웃 활성화. (공통 세그먼트의 일부 경로에 레이아웃 추가)
URL 에 영향을 주지 않고 경로 구성하기 Organize routes without affecting the URL path
- URL 에 영향 없이 경로를 구성하려면 관련 경로를 함께 유지하기 위해 그룹을 만든다.
- 괄호 안의 폴더는 URL 에서 생략된다. (e.g. (marketing) 또는 (shop))
- (marketing) 및 (shop) 내부의 경로는 동일한 URL 계층 구조를 공유하지만, 각 그룹 내 layout.js 파일을 추가해 각 그룹에 대해 다른 레이아웃을 생성할 수 있다.
- 라우트 그룹을 포함하는 경로는 다른 경로와 동일한 URL 경로여선 안된다.
예시로 (marketing)/about/page.js 와 (shop)/about/page.js 는 모두 /about으 로 해결되어 오류를 일으킨다.
여러 개의 루트 레이아웃 생성 Creating multiple root layouts
- 여러 루트 레이아웃을 만들려면 최상위 layout.js 파일을 제거하고 각 라우트 그룹 내에 layout.js 파일을 추가한다.
이 경우 홈 page.js 파일은 라우트 그룹 중 하나에 정의되어야 한다. app/(marketing)/page.js - 이는 완전히 다른 UI 나 경험을 제공하는 섹션으로 애플리케이션을 분할하는 데 유용하다.
- 각 루트 레이아웃에는 <html> 및 <body> 태그가 추가되어야 한다.
- 여러 루트 레이아웃 간의 내비게이션은 전체 페이지 로드를 유발한다. (클라이언트 측 내비게이션과 반대)
예시로, app/(shop)/layout.js 를 사용하는 /cart 에서 app/(marketing)/layout.js 를 사용하는 /blog 로 내비게이션 하는 경우 전체 페이지 로드가 발생한다. 이는 오직 여러 루트 레이아웃에 적용된다.
8. 코로케이션(배치) Colocation
Next.js 는 라우팅 폴더 및 파일 규칙 외엔 프로젝트 파일을 조직하고 배치하는 방법에 대한 의견이 없다.
기본적으로 안전한 배치 Safe colocation by default
- app 디렉토리에서 중첩된 폴더 계층 구조는 라우트 구조를 정의한다.
- 각 폴더는 URL 경로에 해당하는 경로 세그먼트를 나타낸다.
- 그러나 경로 구조가 폴더를 통해 정의되더라도 page.js 또는 route.js 파일이 경로 세그먼트에 없다면 공개적으로 접근 불가하다.
- 경로가 공개적으로 접근 가능하게 되어도 page.js 또는 route.js 에 의해 반환된 콘텐츠만 클라이언트에 전송된다.
- 이것은 프로젝트 파일이 app 디렉토리의 경로 세그먼트 내에서 안전하게 배치되어 실수로 라우팅되지 않도록 할 수 있음을 의미한다.
프로젝트 조직 기능 Project organization features
Next.js 는 프로젝트를 조직하는 데 도움이 되는 몇 가지 기능을 제공한다.
비공개 폴더 Private Folders
- 폴더 이름 앞에 밑줄을 추가하여 생성할 수 있다. _forderName
- 이는 폴더가 비공개 구현 사항임을 나타내며, 라우팅 시스템에서 고려되지 않도록 하여 폴더와 해당 하위 폴더를 라우팅에서 제외한다.
- app 디렉토리 파일은 기본적으로 안전하게 배치 가능하여 비공개 폴더는 배치에 불필요하지만, 이는 다음과 같은 경우 유용할 수 있다.
- UI 로직을 라우팅 로직과 문리
- 프로젝트와 Next.js 생태계 전반에 걸쳐 내부 파일을 일관되게 조직
- 코드 편집기에서 파일 정렬 및 그룹화
- 향후 Next.js 파일 규칙과의 잠재적 이름 충돌 방지
- 프레임워크 규칙은 아니지만 비공개 폴더 외부 파일을 밑줄을 사용해 '비공개'로 표시하는 것도 고려할 수 있다.
- 폴더 이름 앞에 %5F(밑줄의 URL 인코딩 형식)를 추가해 밑줄로 시작하는 URL 세그먼트를 만들 수 있다.
src 디렉토리 Directory
- Next.js 는 선택적 src 디렉토리 내에 애플리케이션 코드(포함 app)를 저장하는 것을 지원한다.
- 이를 통해 애플리케이션 코드를 프로젝트 구성 파일과 분리할 수 있다.
모듈 경로 별칭 Moudile Path Aliases
- Next.js 는 모듈 경로 별칭을 지원해 깊게 중첩된 프로젝트 파일 간의 가져오기를 더 쉽게 읽고 유지 관리할 수 있다.
// before
import { Button } from '../../../components/button'
// after
import { Button } from '@/components/button'
프로젝트 조직 전략 Project organization strategies
- Next.js 프로젝트에서 파일과 폴더를 조직하는 데 '옳거나 그른' 방법은 없다.
- 다음은 일반적인 방식을 소개하는데, 가장 중요한 건 각자에게 맞는 전략을 선택하고 프로젝트 전반에 걸쳐 일관성을 유지하는 것이다.
- 아래 예제에선 components 와 lib 폴더를 일반적인 자리 표시자로 사용하며, 그 이름은 특별한 프레임워크 의미가 없으며 프로젝트에서는 ui, utils, hooks, styles 등 다른 폴더를 사용할 수 있다.
1. 앱 외부에 프로젝트 파일 저장 Store project files outside of app
- 이 방식은 모든 애플리케이션 코드를 루트 공유 폴더에 저장하고 app 디렉토리는 순전히 라우팅 목적으로 유지한다.
2. 앱 내 최상위 폴더에 프로젝트 파일 저장 Store project files in top-level folders inside of app
- 이 방식은 모든 애플리케이션 코드를 app 디렉토리 루트의 공유 폴더에 저장한다.
3. 기능 또는 경로별로 프로젝트 파일 분할 Split project files by feature or route
- 이 방식은 루트 app 디렉토리에 전역적으로 공유되는 애플리케이션 코드를 저장하고, 특정 경로 세그먼트에 사용되는 애플리케이션 코드를 분할한다.
9. 동적 라우트 Dynamic Routes
동적 라우트를 사용하면 동적 데이터를 기반으로 프로그래매틱하게 라우트 세그먼트를 생성할 수 있다.
- 동적 세그먼트는 폴더 이름을 대괄호로 감싸 만들 수 있다. [forderName]
- 동적 세그먼트는 params prop 으로 layout, page, route 및 generateMetadata 함수에 전달된다.
- 예시로 블로그는 [slug] 가 블로그 게시물의 동적 세그먼트인 app/blog/[slug]/page.js 라우트를 포함할 수 있다.
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>
} // params => { slug: 'a' }, URL => /blog/a
정적 매개변수 생성 Generating Static Params
- generateStaticParams 함수는 동적 경로 세그먼트와 결합해 빌드 시 정적 생성 라우트를 생성하는 데 사용할 수 있다.
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
- 이 함수의 주요 이점은 데이터의 스마트한 검색이다. 함수 내에서 fetch 요청으로 콘텐츠를 검색하면 요청이 자동으로 메모이제이션된다. 즉 여러 generateStaticParams, 레이아웃 및 페이지에서 동일한 인수를 사용하는 fetch 요청은 한 번만 수행되어 빌드 시간을 단축한다.
Catch-all 세그먼트 Catch-all Segments
- 동적 세그먼트는 대괄호 안에 줄임표를 추가해 모든 후속 세그먼트를 포괄하도록 확장할 수 있다. [...forderName]
- 예시로, app/shop/[...slug]/page.js 는 /shop/clothes, /shop/clothes/tops, /shop/clothes/tops/t-shirts 등과 일치한다.
Route | Example URL | params |
app/shop/[...slug]/page.js | /shop/a | { slug: ['a'] } |
app/shop/[...slug]/page.js | /shop/a/b | { slug: ['a', 'b'] } |
app/shop/[...slug]/page.js | /shop/a/b/c | { slug: ['a', 'b', 'c'] } |
선택적 Catch-all 세그먼트 Optional Catch-all Segments
- Catch-all 세그먼트는 매개변수를 이중 대괄호로 포함해 선택 사항으로 만들 수 있다. [[...forderName]]
- 예시로, app/shop/[[...slug]]/page.js 는 /shop/clothes, /shop/clothes/tops, /shop/clothes/tops/t-shirts, /shop 과 일치한다.
- catch-all 과 optional catch-all 세그먼트의 차이점은 optional 의 경우 매개변수 없이도 경로가 일치한다는 점이다. (위 예제의 /shop)
Route | Example URL | params |
app/shop/[[...slug]]/page.js | /shop | {} |
app/shop/[[...slug]]/page.js | /shop/a | { slug: ['a'] } |
app/shop/[[...slug]]/page.js | /shop/a/b | { slug: ['a', 'b'] } |
app/shop/[[...slug]]/page.js | /shop/a/b/c | { slug: ['a', 'b', 'c'] } |
10. 병렬 라우트 Parallel Routes
병렬 라우트를 사용하면 동일한 레이아웃 내에서 하나 이상의 페이지를 동시에 또는 조건부로 렌더링할 수 있다.
- 이는 소셜 사이트의 대시보드 및 피드와 같은 매우 동적인 앱 섹션에 유용하다.
- 예시로, 대시보드를 고려할 때 병렬 라우트를 사용해 team 및 analytics 페이지를 동시에 렌더링할 수 있다.
슬롯 Slots
- 병렬 라우트는 슬롯이라는 이름으로 생성된다. 슬롯은 @folder 규칙으로 정의된다.
- 예시로, 다음 파일 구조는 두 개의 슬롯인 @analytics 와 @team 을 정의한다.
- 슬롯은 공유된 부모 레이아웃에 props 로 전달된다. 위 예시에선 app/layout.js 의 컴포넌트가 이제 @analytics 및 @team slots props 를 받아들이고, children prop 과 함께 병렬로 렌더링할 수 있다.
- 하지만 슬롯은 라우트 세그먼트가 아니며 URL 구조에 영향을 미치지 않는다. 예시로, /@analytics/views 의 경우 URL 은 @analytics 가 슬롯이므로 /view 가 된다.
- children prop 은 폴더에 매핑될 필요가 없는 암묵적 슬롯이다. 이는 app/page.js 가 app/@children/page.js 와 동일함을 의미.
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
활성 상태 및 탐색 Active state & navigation
기본적으로 Next.js 는 각 슬롯에 대한 활성 state (또는 하위 페이지)를 추적한다. 그러나 슬롯 내에서 렌더링되는 콘텐츠는 탐색 유형에 따라 달라진다.
- Soft Navigation : 클라이언트 측 탐색 중에 Next.js 는 부분 렌더링을 수행하여 현재 URL 과 일치하지 않더라도 다른 슬롯의 활성 하위 페이지를 유지하면서 슬롯 내의 하위 페이지를 변경한다.
- Hard Navigation : 전체 페이지 로드(브라우저 새로고침) 후 Next.js 는 현재 URL 과 일치하지 않는 슬롯의 활성 상태를 결정할 수 없다. 대신, 일치하지 않는 슬롯에 대해 default.js 파일을 렌더링하거나, default.js 가 없으면 404 를 렌더링한다.
- 일치하지 않는 경로에 대한 404 는 의도하지 않은 페이지에 Parallel Route 가 렌더링되지 않도록 보장한다.
default.js
- 초기 로드 또는 전체 페이지 새로고침 시 일치하지 않는 슬롯에 대한 대체 렌더링을 위해 default.js 파일을 정의할 수 있다.
- 아래 폴더 구조를 고려하라. @team 슬롯에는 /settings 페이지가 있지만, @analytics 에는 없다.
- /settings 로 이동할 때, @team 슬롯은 /settings 페이지를 렌더링하면서 @analytics 슬롯의 현재 활성 페이지를 유지한다.
- 새로고침 시, Next.js 는 @analytics 에 대해 default.js 를 렌더링한다. default.js 가 없으면 대신 404 가 렌더링된다.
- 추가로 children 은 암묵족 슬롯이므로 Next.js 가 부모 페이지의 활성 상태를 복구할 수 없을 때 children 에 대한 대체 렌더링을 위해 default.js 파일을 생성해야 한다.
useSelectedLayoutSegment(s)
- useSelectedLayoutSegment 및 useSelectedLayoutSegments 는 parallelRoutesKey 매개변수를 받아 슬롯 내 활성 라우트 세그먼트를 읽을 수 있게 한다.
- 사용자가 app/@auth/login (또는 URL 바의 /login)으로 이동하면, loginSegment 는 문자열 'login'과 동일하다.
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function Layout({ auth }: { auth: React.ReactNode }) {
const loginSegment = useSelectedLayoutSegment('auth')
// ...
}
조건부 라우트 Conditional 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}</>
}
탭 그룹 Tab Groups
- 사용자가 슬롯을 독립적으로 탐색할 수 있도록 슬롯 내 layout 을 추가할 수 있다. 이는 탭을 만드는 데 유용하다.
- 예시로, @analytics 슬롯 내에는 두 개의 하위 페이지 /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>
</>
)
}
모달 Modals
- 병렬 라우트는 Intercepting Routes(아래에서 설명) 와 함께 사용해 딥 링크를 지원하는 모달을 생성할 수 있따.
- 이를 통해 모달을 구축할 때 일반적으로 발생하는 문제를 해결할 수 있다.
- 모달 콘텐츠를 URL을 통해 공유 가능하게 만들기.
- 페이지를 새로 고침할 때 컨텍스트를 유지하여 모달을 닫지 않기.
- 이전 탐색 시 모달 닫기.
- 앞으로 탐색 시 모달 다시 열기.
- 사용자가 클라이언트 측 탐색을 사용해 레이아웃에서 로그인 모달을 열거나 별도의 /login 페이지에 접근할 수 있는 UI 를 생각해보자.
- 이 UI 패턴을 구현하려면, 먼저 주요 로그인 페이지를 렌더링하는 /login 라우트를 생성한다. /login/page.tsx
- 그 다음, @auth 슬롯 내에 default.js 파일을 추가해 모달이 활성 상태가 아닐 때 렌더링되지 않도록 한다.
export default function Default() {
return '...'
}
- @auth 슬롯 내에서 /(.)login 폴더를 업데이트하여 /login 라우트를 가로챈다.
- 라우트를 가로채기 위해 사용하는 규칙(e.g. (.))는 파일 시스템 구조에 따라 다르다. 🔗
- /(.)login/page.tsx 파일에 <Modal> 컴포넌트와 자식을 가져온다.
- 모달 콘텐츠(<Login>)에서 <Modal> 기능을 분리함으로써 모달 내 모든 콘텐츠(e.g. forms)가 서버 컴포넌트가 되도록 할 수 있다. 🔗
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'
export default function Page() {
return (
<Modal>
<Login />
</Modal>
)
}
모달 열기
- 이제 Next.js 라우터를 활용해 모달을 열고 닫을 수 있다.
- 이를 통해 모달이 열릴 때와 이전 및 이후 탐색 시 URL 이 올바르게 업데이트되도록 보장할 수 있다.
- 모달을 열려면 부모 레이아웃에 @auth 슬롯을 prop 으로 전달하고 children prop 과 함께 렌더링한다.
- 사용자가 <Link> 를 클릭하면 /login 페이지로 이동하는 대신 모달이 열린다. 그러나 새로고침 또는 초기 로드 시 /login 으로 이동하면 주요 로그인 페이지로 이동한다.
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>
</>
)
}
모달 닫기
- 모달을 닫으려면 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>
</>
)
}
- Link 컴포넌트를 사용해 더 이상 @auth 슬롯을 렌더링하지 않아야 하는 페이지로 이동할 때는 평행 라우트가 null 을 반환하는 컴포넌트와 일치하도록 해야 한다.
- 예를 들어, 루트 페이지로 돌아갈 때 @auth/page.tsx 컴포넌트를 생성한다.
- 또는 다른 페이지(e.g. /foo, /foo/bar 등)로 이동할 때 catch-all 슬롯을 사용할 수 있다.
import Link from 'next/link'
export function Modal({ children }: { children: React.ReactNode }) {
return (
<>
<Link href="/">Close modal</Link>
<div>{children}</div>
</>
)
}
export default function CatchAll() {
return '...'
}
로딩 및 에러 UI Loading & Error UI
- 병렬 라우트는 독립적으로 스트리밍될 수 있으므로 각 라우트에 대해 독립적인 오류 및 로딩 상태를 정의할 수 있다.
11. 경로 가로채기 Intercepting Routes
인터셉팅 라우트를 사용하면 현재 레이아웃 내에서 애플리케이션의 다른 부분의 라우트를 로드할 수 있다.
- 이 라우팅 패러다임은 사용자가 다른 컨텐스트로 전환하지 않고도 라우트 콘텐츠를 표시하려는 경우에 유용하다.
- 예시로, 피드에서 사진을 클릭할 때 사진을 피드 위에 모달로 오버레이하여 표시할 수 있다.👇
이 경우, Next.js 는 /photo/123 라우트를 가로채고 URL 을 마스킹하여 /feed 위에 오버레이한다. - 하지만 공유 가능한 URL 을 클릭하거나 페이지를 새로고침하여 사진으로 이동하는 경우에는 전체 사진 페이지가 모달 대신 렌더링되어야 한다. 이때는 인터셉팅 라우트가 발생하지 않는다.
- 인터셉팅 라우트는 (..) 규칙으로 정의할 수 있으며, 이는 세그먼트에 대해 상대 경로 규칙 ../ 과 유사하다.
- 다음과 같은 방법으로 사용 가능하다.
- (.)는 동일 레벨의 세그먼트를 매칭한다.
- (..)는 한 레벨 위의 세그먼트를 매칭한다.
- (..)(..)는 두 레벨 위의 세그먼트를 매칭한다.
- (...)는 루트 app 디렉토리의 세그먼트를 매칭한다.
- 예시로, feed 세그먼트 내 photo 세그먼트를 가로채려면 (..)photo 디렉토리를 생성할 수 있다.
- (..) 규칙은 라우트 세그먼트를 기반으로 하며, 파일 시스템을 기반으로 하지 않는다는 점에 유의하자.
모달 Modals
- 인터셉팅 라우트는 병렬 라우트와 함께 사용되어 모달을 생성할 수 있다.
- 사용자가 클라이언트 측 탐색을 사용하여 갤러리에서 사진 모달을 열거나 공유 가능한 URL 에서 직접 사진 페이지로 이동할 수 있는 UI를 생각해 보자.👇
- 아래 예제에서 photo 세그먼트의 경로는 (..) 매체를 사용할 수 있다. @modal 은 세그먼트가 아니기 때문이다. 즉, 파일 시스템 상으로는 두 레벨 위에 있지만 라우트 세그먼트 상으로는 한 레벨 위에 있다.
- 다른 예로 상단 Nav 바에서 로그인 모달을 열거나 전용 /login 페이지를 가지는 경우, 사이드 모달에서 쇼핑 카트를 여는 경우가 있다.
12. 라우트 핸들러 Route Handlers
라우트 핸들러를 사용하면 Request 와 Response API 를 사용해 특정 라우트에 대한 사용자 정의 요청 핸들러를 만들 수 있다.
- 라우트 핸들러는 app 디렉토리 내에서만 사용할 수 있다. 이는 pages 디렉토리 내 API Routes 와 동일한 기능을 한다.
- 라우트 핸들러는 app 디렉토리 내 route.js | ts 파일로 정의된다.
- 라우트 핸들러는 page.js 및 layout.js 와 유사하게 app 디렉토리 내 어디에나 중첩될 수 있다. 하지만 page.js 와 동일한 라우트 세그먼트 레벨에 route.js 파일이 있을 순 없다.
// app/api/route.ts
export async function GET(request: Request) {}
HTTP 메서드 지원 Supported HTTP Methods
- GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS 메서드를 지원한다.
- 지원되지 않는 메서드가 호출되면 Next.js 는 405 Method Not Allowed 응답을 반환한다.
확장된 NextRequest 및 NextResponse API Exteded NextRequest & NextResponse APIs
- Next.js는 기본 Request 및 Response API 지원 외에도, NextRequest 와 NextResponse 를 확장해 고급 사용 사례를 위한 편리한 헬퍼를 제공한다.
동작 Behavior
Cacing
- 라우트 핸들러는 기본적으로 캐시되지 않는다. 그러나 GET 메서드에 대해 캐시를 선택할 수 있다.
- 이를 위해 라우트 핸들러 파일에 export const dynamic = 'force-static' 과 같은 route config option 을 사용한다.
export const dynamic = 'force-static'
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
})
const data = await res.json()
return Response.json({ data })
}
특별한 라우트 핸들러 Special Route Handlers
- sitemap.ts, opengraph-image.tsx, icon.tsx 및 기타 메타데이터 파일과 같은 특별한 라우트 핸들러는 동적 함수나 동적 구성 옵션을 사용하지 않는 한 기본적으로 정적이다.
경로 확인 Route Resolution
- route 는 가장 낮은 레벨의 라우팅 원시 형태로 간주할 수 있다.
- page 와 같은 레이아웃이나 클라이언트 측 탐색에 참여하지 않는다.
- page.js 와 동일한 라우트에 route.js 파일이 있을 수 없다.
Page | Route | Result |
app/page.js | app/route.js | Conflict ❎ |
app/page.js | app/api/route.js | Valid ✅ |
app/[user]/page.js | app/api/route.js | Valid ✅ |
- 각 route.js 또는 page.js 파일은 해당 라우트에 대한 모든 HTTP 메서드를 처리한다.
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
// ❌ Conflict
// `app/route.js`
export async function POST(request) {}
아래 예제는 라우트 핸들러를 다른 Next.js API 및 기능과 결합하는 방법을 보여준다.
캐시된 데이터 재검증 Revalidating Cached Data
- next.revalidate 옵션을 사용해 캐시된 데이터를 재검증할 수 있다.
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
next: { revalidate: 60 }, // 60초마다 재검증
})
const data = await res.json()
return Response.json(data)
}
- 또는 revalidate 세그먼트 구성 옵션을 사용할 수 있다.
export const revalidate = 60
동적 함수Dynamic Funcions
- 라우트 핸들러는 Next.js 의 cookies 및 headers 와 같은 동적 함수와 함께 사용할 수 있다.
쿠키 Cookies
- next/hdaders 에서 cookies 를 사용해 쿠키를 읽거나 설정할 수 있다. 이 서버 함수는 Route Handler 에서 직접 호출되거나 다른 함수 내에서 중첩될 수 있다.
- 또는 Set-Cookie 헤더를 사용해 새 Response 를 반환할 수 있다.
import { cookies } from 'next/headers'
export async function GET(request: Request) {
const cookieStore = cookies()
const token = cookieStore.get('token')
return new Response('Hello, Next.js!', {
status: 200,
headers: { 'Set-Cookie': `token=${token.value}` },
})
}
헤더 Headers
next/headers 에서 headers 를 사용해 헤더를 읽을 수 있다. 이 서버 함수는 라우트 핸들러에서 직접 호출되거나 다른 함수 내에서 중첩될 수 있다.
이 headers 인스턴스는 읽기 전용이다. 헤더를 설정하려면 새 headers 와 함께 새 Response 를 반환해야 한다.
import { headers } from 'next/headers'
export async function GET(request: Request) {
const headersList = headers()
const referer = headersList.get('referer')
return new Response('Hello, Next.js!', {
status: 200,
headers: { referer: referer },
})
}
- 기본 웹 API 를 사용해 요청에서 쿠키나 헤더를 읽을 수도 있다. (NextRequest)
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const token = request.cookies.get('token')
}
export async function GET(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
}
리다이렉션 Redirects
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
redirect('https://nextjs.org/')
}
동적 라우트 세그먼트 Dynamic Route Segments
- 라우트 핸들러는 다이나믹 세그먼트를 사용해 도적 데이터를 기반으로 요청 핸들러를 생성할 수 있다.
export async function GET(
request: Request,
{ params }: { params: { slug: string } },
) {
const slug = params.slug // 'a', 'b', 또는 'c'
}
URL 쿼리 파라미터 URL Query Parameters
- 라우트 핸들러에 전달되는 요청 객체는 NextRequest 인스턴스로, 쿼리 매개변수를 보다 쉽게 처리할 수 있는 편리한 추가 메서드를 제공한다.
import { type NextRequest } from 'next/server'
export function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
// query는 /api/search?query=hello일 때 "hello"입니다.
}
스트리밍 Streaming
- 스트리밍은 OpenAI 와 같은 대형 언어 모델(LLM)과 함께 AI 생성 콘텐츠를 위해 일반적으로 사용된다. 🔗
요청 본문 Reauest Body
- 기본 웹 API 메서드를 사용해 Request 본문을 읽을 수 있다.
export async function POST(request: Request) {
const res = await request.json()
return Response.json({ res })
}
요청 본문 양식 데이터 Request Body FormData
- request.formData() 함수를 사용해여 FormData 를 읽을 수 있다.
- fromData 데이터는 모두 문자열이므로 zod-form-data 를 사용해 요청을 검증하고 원하는 형식(e.g. number)으로 데이터를 가져올 수 있다.
export async function POST(request: Request) {
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
return Response.json({ name, email })
}
교차 출처 리소스 공유 CORS Cross Origin Resource Sharing
- 서버가 다른 출처로부터의 액세스를 허용하거나 제한할 수 있게 하는 HTTP 헤더 기반의 메커니즘
- 기본 웹 API 메서드를 사용해 특정 라우트 핸들러에 대한 CORS 헤더를 설정할 수 있다.
- 여러 라우트 핸들러에 CORS 헤더를 추가하려면 미들웨어 또는 next.config.js 파일을 사용할 수 있다.
export async function GET(request: Request) {
return new Response('Hello, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
웹훅 Webhooks
- 특정 이벤트가 발생했을 때, 미리 설정된 URL로 자동으로 HTTP 요청을 보내는 기능
- 라우트 핸들러를 사용해 타사 서비스의 웹훅을 수신할 수 있다.
export async function POST(request: Request) {
try {
const text = await request.text()
// 웹훅 페이로드 처리
} catch (error) {
return new Response(`Webhook error: ${error.message}`, {
status: 400,
})
}
return new Response('Success!', {
status: 200,
})
}
UI 가 아닌 콘텐츠 반환 Non-UI Responses
- 라우터를 사용해 UI 가 아닌 콘텐츠를 반환할 수 있다.
- sitemap.xml, robots.txt, app icons, open graph images 등은 모두 기본적으로 지원된다.
export async function GET() {
return new Response(
`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Next.js Documentation</title>
<link>https://nextjs.org/docs</link>
<description>The React Framework for the Web</description>
</channel>
</rss>`,
{
headers: {
'Content-Type': 'text/xml',
},
},
)
}
세그먼트 구성 옵션 Segment Config Options
- 라우트 핸들러는 페이지 및 레이아웃과 동일한 route segment configuration 을 사용한다.
export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
13. 미들웨어 Middleware
미들웨어를 사용하면 요청이 완료되기 전에 코드를 실행할 수 있다. 그런 다음, 들어오는 요청에 따라 응답을 재작성, 리다이렉션, 요청 및 응답 헤더 수정, 또는 직접 응답할 수 있다. 미들웨어는 캐시된 콘텐츠와 라우트가 일치하기 전에 실행된다.
- 프로젝트 루트에 middleware.ts | js 파일을 사용해 미들웨어를 정의한다. 예를 들어 page 또는 app 과 같은 수준에 있거나 src 내에 있을 수 있다.
- 프로젝트 당 하나의 middleware.ts 파일만 지원되지만, 여전히 모듈화된 방식으로 미들웨어 로직을 구성할 수 있다.
미들웨어 기능을 별도의 파일로 분리하고 주요 middleware.ts 파일에 가져올 수 있다. 그럼 경로별 미들웨어를 보다 깔끔하게 관리할 수 있으며, 중앙 집중식 제어를 위해 middleware.ts 에 집계할 수 있다. 단일 미들웨어 파일을 강제함으로써 구성의 간소화, 잠재적 충돌 방지 및 여러 미들웨어 계층을 피해 성능 최적화를 할 수 있다.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// 이 함수는 내부에서 `await`를 사용하는 경우 `async`로 표시될 수 있습니다
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// 아래 "Matching Paths"를 참조하여 자세히 알아보세요
export const config = {
matcher: '/about/:path*',
}
미들웨어가 특히 효과적인 일반적 사용 사례 Use Cases
애플리케이션에 미들웨어를 통합하면 성능, 보안 및 사용자 경험이 크게 향상될 수 있다.
- 인증 및 권한 부여 : 사용자 신원을 확인하고 특정 페이지나 API 라우트에 접근하기 전에 세션 쿠키를 확인한다.
- 서버 측 리디렉션 : 특정 조건(e.g. 로케일, 사용자 역할)에 따라 서버 수준에서 사용자를 리다이렉션한다.
- 경로 재작성 : 요청 속성에 따라 경로를 동적으로 재작성하여 A/B 테스트, 기능 출시 또는 레거시 경로를 지원한다.
- 봇 감지 : 리소스를 보호하기 위해 봇 트래픽을 감지하고 차단한다.
- 로깅 및 분석 : 페이지나 API 가 처리되기 전에 요청 데이터를 캡처하고 분석한다.
- 기능 플래깅 : 기능을 동적으로 활성화 또는 비활성화하여 원활한 기능 출시 또는 테스트를 진행한다.
미들웨어가 최적의 접근 방식이 아닌 상황을 인식하는 것도 중요하다. 주의해야할 사례는 다음과 같다.
- 복잡한 데이터 가져오기 및 조작 : 미들웨어는 직접적인 데이터 가져오기나 조작을 위해 설계되지 않았으며, 이는 라우트 핸들러 또는 서버 측 유틸리티 내에서 수행해야 한다.
- 무거운 계산 작업 : 미들웨어는 가볍고 빠르게 응답해야 하며, 무거운 계산 작업이나 장기 실행 프로세스는 전용 라우트 핸들러 내에서 수행해야 한다.
- 광범위한 세션 관리 : 미들웨어는 기본적인 세션 작업을 관리할 수 있지만, 광범위한 세션 관리는 전용 인증 서비스 또는 라우트 핸들러 내에서 관리해야 한다.
- 직접적인 데이터베이스 작업 : 미들웨어 내에서 직접적인 데이터베이스 작업을 수행하는 것은 권장되지 않는다. 데이터베이스 상호작용은 라우트 핸들러 또는 서버 측 유틸리티 내에서 수행해야 한다.
경로 매칭하기 Matching Paths
미들웨어는 프로젝트의 모든 라우트에 대해 호출된다. 따라서, 특정 라우트를 정확히 타겟팅하거나 제외하기 위해 매처를 사용하는 것이 중요하다. 실행 순서는 다음과 같다.
- next.config.js의 headers
- next.config.js의 redirects
- Middleware (rewrites, redirects 등)
- next.config.js의 beforeFiles (rewrites)
- 파일 시스템 라우트 (public/, _next/static/, pages/, app/ 등)
- next.config.js의 afterFiles (rewrites)
- 동적 라우트 (/blog/[slug])
- next.config.js의 fallback (rewrites)
아래에선 미들웨어가 실행될 경로를 정의하는 두 가지 방법을 설명한다.
1. 사용자 지정 매처 구성 Custom matcher config(Matcher)
- 매처를 사용해 특정 경로에서만 미들웨어를 실행하도록 필터링할 수 있다.
- 단일 경로나 여러 경로를 배열 구문으로 매칭할 수 있다.
- 매처 값은 빌드 시 정적으로 분석될 수 있도록 상수여야 한다. 변수와 같은 동적 값은 무시된다.
- / 로 시작해야 한다.
- 명명된 매개변수를 포함 가능 : /about/:path 는 /about/a 및 /about/b 와 일치하지만 /about/a/c 와는 일치하지 않는다.
- 명명된 매개변수( :로 시작)에 수정자 포함 가능 : /about/:path* 는 * 이 0개 이상이므로 /about/a/b/c 와 일치한다.
? 는 0개 또는 1개, + 는 1개 이상 - 괄호로 둘러싸인 정규식 사용 가능 : /about/(.*) 는 /about/:path* 와 동일하다.
- 이전 버전과의 호환성을 위해 Next.js 는 항상 /public 을 /public/index로 간주한다. 따라서 /public/:path 의 매처는 일치한다.
export const config = {
matcher: '/about/:path*',
}
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
- 매처 구성은 전체 정규식을 허용하므로, 부정형 전방탐색이나 문자 매칭과 같은 정규식 매칭이 지원된다.
- 특정 경로를 제외하고 모든 경로를 매칭하는 부정형 전방탐색의 예는 다음과 같다.
export const config = {
matcher: [
/*
* 다음으로 시작하는 경로를 제외한 모든 요청 경로를 매칭합니다:
* - api (API 라우트)
* - _next/static (정적 파일)
* - _next/image (이미지 최적화 파일)
* - favicon.ico (파비콘 파일)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
- 또한 missing 또는 has 배열 또는 두 가지 조합을 사용해 특정 요청을 위해 미들웨어를 우회할 수 있다.
export const config = {
matcher: [
/*
* 다음으로 시작하는 경로를 제외한 모든 요청 경로를 매칭합니다:
* - api (API 라우트)
* - _next/static (정적 파일)
* - _next/image (이미지 최적화 파일)
* - favicon.ico (파비콘 파일)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
has: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
has: [{ type: 'header', key: 'x-present' }],
missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
},
],
}
2. 조건문 Conditional Statements
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
NextResponse
NextResponse API 를 사용해 다음 작업을 수행할 수 있다.
- 들어오는 요청을 다른 URL로 redirect
- 주어진 URL을 표시하여 응답을 rewrite
- API Routes, getServerSideProps, 및 rewrite 대상에 대해 요청 헤더 설정
- 응답 쿠키 설정
- 응답 헤더 설정
미들웨어에서 응답을 생성하려면 다음을 수행할 수 있다.
- 응답을 생성하는 라우트 (Page 또는 Route Handler) 로 rewrite
- NextResponse 를 직접 반환한다. Prodicing a Response 참고
쿠키 사용하기 Using Cookies
쿠키는 일반 헤더이다. Request 에서는 Cookie 헤더에 저장되며, Response 에선 Set-Cookie 헤더에 저장된다.
Next.js 는 NextRequest 와 NextResponse 에서 쿠키에 쉽게 접근하고 조작할 수 있는 편리한 방법을 제공한다.
- 들어오는 요청의 경우, cookies 는 get, getAll, set, delete 메서드를 제공한다. has 를 사용해 쿠키의 존재 여부를 확인하거나 clear 를 사용해 모든 쿠키를 제거할 수 있다.
- 나가는 응답의 경우, cookies 는 get, getAll, set, delete 메서드를 제공한다.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 들어오는 요청에 "Cookie:nextjs=fast" 헤더가 있다고 가정합니다
// `RequestCookies` API를 사용하여 요청에서 쿠키를 가져옵니다
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// `ResponseCookies` API를 사용하여 응답에 쿠키를 설정합니다
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// 나가는 응답에는 `Set-Cookie:vercel=fast;path=/` 헤더가 포함됩니다.
return response
}
헤더 설정 Setting Headers
NextResponse API 를 사용해 요청 및 응답 헤더를 설정할 수 있다. * 요청 헤더 설정은 Next.js v13.0.0부터 사용 가능
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 요청 헤더를 복제하고 새로운 헤더 `x-hello-from-middleware1`을 설정합니다
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// NextResponse.next에서 요청 헤더를 설정할 수도 있습니다
const response = NextResponse.next({
request: {
// 새로운 요청 헤더
headers: requestHeaders,
},
})
// 새로운 응답 헤더 `x-hello-from-middleware2`를 설정합니다
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
CORS
- 미들웨어에서 CORS 헤더를 설정해 교차 출처 요청을 허용할 수 있으며, 여기에는 단순 요청 및 사전 요청이 포함된다.
import { NextRequest, NextResponse } from 'next/server'
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export function middleware(request: NextRequest) {
// 요청의 origin을 확인합니다
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)
// 사전 요청을 처리합니다
const isPreflight = request.method === 'OPTIONS'
if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}
// 단순 요청을 처리합니다
const response = NextResponse.next()
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}
Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/api/:path*',
}
응답 생성 Producing a Response
미들웨어에서 Response 또는 NextResponse 인스턴스를 반환해 직접 응답할 수 있다. * 이는 Next.js v13.1.0부터 사용 가능
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
// Middleware를 `/api/`로 시작하는 경로로 제한합니다
export const config = {
matcher: '/api/:function*',
}
export function middleware(request: NextRequest) {
// 요청을 확인하기 위해 인증 함수를 호출합니다
if (!isAuthenticated(request)) {
// 오류 메시지를 나타내는 JSON으로 응답합니다
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 },
)
}
}
waitUntil & NextFetchEvent
- NextFetchEvent 객체는 기본 FetchEvent 객체를 확장하며 waitUntil() 메서도를 포함한다.
- 이 메서드는 프로미스를 인수로 받아 프로미스가 해결될 때까지 미들웨어의 수명을 연장한다.
- 이는 백그라운드에서 작업을 수행하는 데 유용하다.
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
export function middleware(req: NextRequest, event: NextFetchEvent) {
event.waitUntil(
fetch('https://my-analytics-platform.com', {
method: 'POST',
body: JSON.stringify({ pathname: req.nextUrl.pathname }),
}),
)
return NextResponse.next()
}
고급 미들웨어 플래그 Advanced Middleware Flags
Next.js v13.1 에서 두 가지 추가 플래그, skipTrailingSlashRedirect 및 skipMiddlewareUrlNormalize 가 도입되어 고급 사용 사례를 처리할 수 있다.
- skipTrailingSlashRedirect는 후행 슬래시를 추가하거나 제거하기 위한 Next.js 리디렉션을 비활성한다. 이를 통해 일부 경로에서는 후행 슬래시를 유지하고 다른 경로에서는 유지하지 않도록 커스텀 처리를 수행할 수 있어 점진적인 마이그레이션을 용이하게 한다.
// next.config.js
module.exports = {
skipTrailingSlashRedirect: true,
}
// middleware.js
const legacyPrefixes = ['/docs', '/blog']
export default async function middleware(req) {
const { pathname } = req.nextUrl
if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
}
// 후행 슬래시 처리 적용
if (
!pathname.endsWith('/') &&
!pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
) {
return NextResponse.redirect(
new URL(`${req.nextUrl.pathname}/`, req.nextUrl),
)
}
}
- skipMiddlewareUrlNormalize 는 Next.js에서 URL 정규화를 비활성화하여 직접 방문과 클라이언트 전환을 동일하게 처리할 수 있게 한다. 일부 고급 사례에서는 이 옵션을 통해 원래 URL을 사용하여 완전한 제어를 제공한다.
module.exports = {
skipMiddlewareUrlNormalize: true,
}
export default async function middleware(req) {
const { pathname } = req.nextUrl
// GET /_next/data/build-id/hello.json
console.log(pathname)
// 플래그 사용 시 /_next/data/build-id/hello.json
// 플래그 사용 안 할 경우 /hello로 정규화됩니다
}
런타임 Runtime
미들웨어는 현재 Edge runtime 만 지원한다. Node.js 런타임은 사용할 수 없다.
14. 국제화 Internationalization
Next.js 는 여러 언어를 지원하도록 콘텐츠의 라우팅 및 렌더링을 구성할 수 있다. 사이트를 다양한 로케일에 적응시키려면 번역된 콘텐츠(지역화)와 국제화된 라우트를 포함해야 한다.
용어 Terminology
Locale : 언어 및 형식 설정을 위한 식별자이다. 일반적으로 사용자의 선호 언어와 가능하면 지리적 지역을 포함한다.
- en-US : 미국에서 사용되는 영어
- nl-NL : 네덜란드에서 사용되는 네덜란드어
- nl : 특정 지역이 없는 네덜란드어
라우팅 개요 Routing Overview
사용자의 브라우저 언어 설정을 사용해 사용할 로케일을 선택하는 것이 좋다.
- 선호 언어 변경 시 애플리케이션으로 들어오는 Accept-Language 헤더가 변경된다.
- 예시로, 다음 라이브러리를 사용하여 들어오는 Request 를 살펴보고 지원할 로케일과 기본 로케일에 따라 선택할 로케일을 결정할 수 있다.
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
let headers = { 'accept-language': 'en-US,en;q=0.5' }
let languages = new Negotiator({ headers }).languages()
let locales = ['en-US', 'nl-NL', 'nl']
let defaultLocale = 'en-US'
match(languages, locales, defaultLocale) // -> 'en-US'
- 라우팅은 서브 경로(/fr/products) 또는 도메인(my-site.fr/products)으로 국제화할 수 있다. 이 정보를 사용해 미들웨어 내에서 로케일에 따라 사용자를 리다이렉션할 수 있다.
import { NextResponse } from "next/server";
let locales = ['en-US', 'nl-NL', 'nl']
// 위와 유사하게 또는 라이브러리를 사용하여 선호하는 로케일을 가져옵니다.
function getLocale(request) { ... }
export function middleware(request) {
// 경로명에 지원되는 로케일이 있는지 확인합니다.
const { pathname } = request.nextUrl
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return
// 로케일이 없으면 리디렉션합니다.
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
// 예: 들어오는 요청이 /products인 경우
// 새 URL은 이제 /en-US/products입니다.
return NextResponse.redirect(request.nextUrl)
}
export const config = {
matcher: [
// 모든 내부 경로 (_next)를 건너뜁니다.
'/((?!_next).*)',
// 선택 사항: 루트 (/) URL에서만 실행
// '/'
],
}
- 마지막으로 app/ 내 모든 특수 파일이 app/[lang] 아래 중첩되도록 한다. 이렇게 하면 Next.js 라우터가 경로에서 다른 로케일을 동적으로 처리하고 lang 매개변수를 모든 레이아웃 및 페이지에 전달할 수 있다.👇
- 루트 레이아웃도 새 폴더에 중첩될 수 있다. app/[lang]/layout.js
// app/[lang]/page.js
// 이제 현재 로케일에 접근할 수 있습니다.
// 예: /en-US/products -> `lang`은 "en-US"입니다.
export default async function Page({ params: { lang } }) {
return ...
}
현지화 로컬라이즈 Localization
사용자의 선호 로케일에 따라 표시되는 콘텐츠를 변경하는 것은 Next.js 에만 국한된 것이 아니다. 아래에 설명된 패턴은 모든 웹 애플리케이션에서 동일하게 작동한다.
- 애플리케이션 내에서 영어와 네덜란드어 콘텐츠를 모두 지원하려 한다고 가정해 보자.
- 우리는 어떤 키를 로컬라이즈된 문자열로 매핑해주는 두 개의 다른 '사전'을 유지할 수 있다.
// dictionaries/en.json
{
"products": {
"cart": "Add to Cart"
}
}
// dictionaries/nl.json
{
"products": {
"cart": "Toevoegen aan Winkelwagen"
}
}
- 그런 다음 요청된 로케일에 대한 번열을 로드하는 getDictionary 함수를 만들 수 있다.
import 'server-only'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export const getDictionary = async (locale) => dictionaries[locale]()
- 현재 선택된 언어를 고려해 레이아웃이나 페이지 내에서 사진을 가져올 수 있다.
import { getDictionary } from './dictionaries'
export default async function Page({ params: { lang } }) {
const dict = await getDictionary(lang) // en
return <button>{dict.products.cart}</button> // Add to Cart
}
- app/ 디렉토리의 모든 레이아웃 및 페이지는 기본적으로 서버 컴포넌트이기 때문에, 번역 파일의 크기가 클라이언트 측 자바스크립트 번들 크기에 영향을 미치는 것을 걱정할 필요가 없다. 이 코드는 서버에서만 실행되며, 결과 HTML 만 브라우저로 전송된다.
정적 경로 생성 Static Generation
주어진 로케일 세트에 대한 정적 경로를 생성하려면 generateStaticParams 를 사용해 페이지나 레이아웃을 설정할 수 있다. 이는 전역적으로 설정할 수 있으며, 예를 들어 루트 레이아웃에서 사용할 수 있다.
export async function generateStaticParams() {
return [{ lang: 'en-US' }, { lang: 'de' }]
}
export default function Root({ children, params }) {
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
)
}
🔗 https://nextjs-ko.org/docs/app/building-your-application/routing
라우팅 기본 사항 – Nextjs 한글 문서
프론트엔드 애플리케이션의 라우팅 기본 사항을 배우세요.
nextjs-ko.org
'Next' 카테고리의 다른 글
[Next] Next.js 공식문서 정리 2 : 데이터 가져오기 Data Fetching, 캐싱, 서버 액션, 뮤테이션, 클로저, 컨벤션, 알아두면 좋은 점 (0) | 2025.02.07 |
---|---|
[React/Next] 리액트 confetti 폭죽 효과 : react-canvas-confetti 🎉 (1) | 2024.04.26 |
[React/Next] 폰트 최적화 : getaddrinfo enotfound fonts.gstatic.com, 웹폰트 깨짐 🥵 (1) | 2024.04.25 |
[Next] Image hostname Error (1) | 2024.03.15 |
[Next] React Query (1) | 2024.03.15 |