FE/NextJS
NextJS 공식문서 파헤치기 (Middleware)
-Daniel-
2024. 12. 17. 17:05
0️⃣ INTRO
- Middleware를 사용하면 요청이 완료되기 전에 코드 실행 가능
- 그 후에 들어오는 요청에 따라 응답을 재작성, 리디렉션, 요청 또는 응답 헤더 수정 또는 직접 응답할 수 있다.
- Middleware는 캐시된 콘텐츠와 라우트가 일치하기 전에 실행된다.
1️⃣ Use Cases
- 일반적인 시나리오
- 인증 및 권한 부여: 사용자 신원을 확인하고 특정 페이지나 API 라우트에 접근하기 전에 세션 쿠키 확인
- 서버 측 리디렉션: 특정 조건에 따라 서버 수ㅜㄴ에서 사용자를 리디렉션
- 경로 재작성: 요청 속성에 따라 경로를 동적으로 재작성하여 A/B 테스트, 기능 출시 또는 레거시 경로를 지원
- 봇 감지: 리소스를 보호하기 위해 봇 트래픽을 감지하고 차단
- 로깅 및 분석: 페이지나 API가 처리되기 전에 요청 데이터를 캡처하고 분석
- 기능 플래깅: 기능을 동적으로 활성화 또는 비활성화하여 원활한 기능 출시 또는 테스트를 진행
- 주의해야 할 시나리오(아래의 시나리오는 Route Handler에서 수행해야 한다)
- 복잡한 데이터 가져오기 및 조작: 직접적인 데이터 가져오기나 조작을 하면 안된다
- 무거운 계산 작업: Middleware는 가볍고 빠르게 응답해야 한다
- 광범위한 세션 관리: 기본적인 세션 작업만 Middleware에서 해야 한다
- 직접적인 데이터베이스 작업: Middleware 내에서 직접적인 데이터베이스 작업을 수행하는 것은 권장되지 않음
2️⃣ Convention
- 프로젝트의 루트에
middleware.ts
(또는.js
) 파일을 사용하여 Middleware를 정의 pages
또는app
과 같은 수준에 있거나src
내에 있을 수 있다.
🚀 TIP
1. 프로젝트당 하나의 middleware.ts 파일만 지원하지만 모듈화하여 미들웨어 로직을 구성할 수 있다.
2. 미들웨어 기능을 별도의 파일로 분리하고 주요 middleware.ts 파일에 import 할 수 있다.
Example
// 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*',
}
4️⃣ Matching Paths
- Middleware는 프로젝트의 모든 라우트에 대해 호출된다.
- 특정 라우트를 정확히 타겟팅하거나 제외하기 위해 Matcher를 사용하는 것이 중요함
- 실행순서
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
)
- Middleware가 실행될 경로를 정의하는 두 가지 방법
- Custom matcher config
- Conditional statements
Matcher
matcher
를 사용하여 특정 경로에서만 Middleware를 실행하도록 필터링 가능matcher
값은 빌드 시 정적으로 분석될 수 있도록 상수여야 한다. (변수와 같은 동적 값은 무시됨)
📌 matcher 문법
1. /로 시작해야 합니다
2. 명명된 매개변수를 포함할 수 있습니다: /about/:path는 /about/a 및 /about/b와 일치하지만 /about/a/c와는 일치하지 않습니다
3. 명명된 매개변수( :로 시작)에 수정자를 가질 수 있습니다: /about/:path*는 *이 0개 이상이므로 /about/a/b/c와 일치합니다. ?는 0개 또는 1개, +는 1개 이상
4. 괄호로 둘러싸인 정규식을 사용할 수 있습니다: /about/(.*)는 /about/:path*와 동일합니다
자세한 내용: https://github.com/pillarjs/path-to-regexp#path-to-regexp
// 특정 경로에서만 실행
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 배열 또는 두 가지 조합을 사용하여 특정 요청을 위해 Middleware를 우회
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' }],
},
],
}
Conditional Statements
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 요청 경로(pathname)가 /about으로 시작하는 경우, 요청 경로를 /about-2로 재작성
// ex) /about -> /about-2
// ex) /about/team -> /about-2
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))
}
}
5️⃣ NextResponse
NextResponse
API를 사용하여 다음의 작업을 수행 가능- 들어오는 요청을 다른 URL로
redirect
- 주어진 URL을 표시하여 응답을
rewrite
- API Routes,
getServerSideProps
,rewrite
대상에 대해 요청 헤더 설정 - 응답 쿠키 설정
- 응답 헤더 설정
- 들어오는 요청을 다른 URL로
- Middleware에서 응답을 생성하는 방법
- 응답을 생성하는 라우트(Page 또는 Route Handler)로
rewrite
NextResponse
를 직접 반환
- 응답을 생성하는 라우트(Page 또는 Route Handler)로
6️⃣ Using Cookies
- Nextjs는
NextRequest
와NextResponse
에서 쿠키에 쉽게 접근하고 조작할 수 있는 방법을 제공한다.
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
}
7️⃣ Setting Headers
NextResponse
API를 사용하여 요청 및 응답 헤더 설정 가능 (요청 헤더 설정은 Nextjs v13.0.0 부터 사용 가능)- 큰 헤더를 설정하면 백엔드 웹 서버 구성에 따라 431 Request Header Fields Too Large(opens in a new tab) 오류가 발생할 수 있으므로 피해야 한다.
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
}
8️⃣ CORS
- Middleware에서 CORS 헤더를 설정 하여 교차 출처 요청을 허용할 수 있다.
- Route Handlers 에서는 개별 라우트에 대한 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*',
}
9️⃣ Producing a Response
- Middleware 에서
Response
또는NextResponse
인스턴스를 반환하여 직접 응답 (Nextjs 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 },
)
}
}
🔟 ETC
waitUntil()
- promise를 인수로 받아 promise가 해결될 때까지 Middleware의 수명을 연장한다.
- 백그라운드에서 작업을 수행하는 데 유용하다.
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
skipMiddlewareUrlNormalize
: 후행 슬래시를 추가하거나 제거하기 위한 Nextjs 리디렉션을 비활성화한다.
// 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),
)
}
}
skipTrailingSlashRedirect
: Nextjs에서 URL 정규화를 비활성화하여 직접 방문과 클라이언트 전환을 동일하게 처리 가능
// next.config.js
module.exports = {
skipMiddlewareUrlNormalize: true,
}
// middleware.js
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
- Middleware는 현재 Edge runtime만 지원한다. Nodejs 런타임은 사용 불가