HTTP 요청이 페이지나 API Route에 도달하기 전, Middleware가 먼저 실행된다. 인증 토큰 검증, 로케일 감지, A/B 테스트 플래그 주입 등 모든 요청에 공통으로 적용해야 하는 로직을 한 곳에서 처리할 수 있다. Next.js 12부터 Edge Runtime 위에서 동작하므로 지연 시간이 거의 없다.
middleware.ts(또는 .js)는 프로젝트 루트 또는 src/ 디렉토리 바로 아래에 위치해야 한다. pages/나 app/ 안에 두면 인식되지 않는다.
project/
├── src/
│ ├── middleware.ts ← 여기
│ └── app/
│ └── page.tsx
└── next.config.ts
요청 흐름은 다음과 같다.
Client Request
│
▼
┌─────────────┐
│ Middleware │ ← 모든 경로에서 먼저 실행
└──────┬──────┘
│ NextResponse.next() / redirect() / rewrite()
▼
┌─────────────┐
│ App Router │ (또는 Pages Router)
└─────────────┘
기본적으로 Middleware는 정적 파일(_next/static, favicon.ico 등)을 제외한 모든 경로에서 실행된다. matcher 설정으로 범위를 명시적으로 좁힌다.
// middleware.ts
export const config = {
matcher: [
'/dashboard/:path*',
'/api/private/:path*',
],
}
matcher: 정규식 기반 경로 필터. 배열로 여러 패턴을 지정할 수 있다.
Middleware 함수는 NextRequest를 받아 NextResponse를 반환한다.
| 메서드 | 설명 |
|---|---|
NextResponse.next | 요청을 그대로 통과 |
NextResponse.redirect(url) | 클라이언트를 다른 URL로 리다이렉트 |
NextResponse.rewrite(url) | URL은 유지하면서 내부적으로 다른 경로를 렌더링 |
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
실제 프로젝트에서 가장 많이 쓰이는 패턴은 JWT 토큰 또는 세션 쿠키 존재 여부를 검사한 뒤 미인증 사용자를 로그인 페이지로 보내는 것이다.
import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
await jwtVerify(token, SECRET)
return NextResponse.next()
} catch {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/dashboard/:path*'],
}
jose: Web Crypto API 기반의 경량 JWT 라이브러리. Edge Runtime에서 사용 가능하다. Node.js 전용인 jsonwebtoken은 Middleware에서 동작하지 않는다.
NextResponse.next에 headers 옵션을 전달해 요청·응답 헤더를 추가하거나 덮어쓸 수 있다. 서버 컴포넌트로 사용자 정보를 전달할 때 유용하다.
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', getUserId(request))
return NextResponse.next({
request: { headers: requestHeaders },
})
}
서버 컴포넌트에서는 headers 함수로 x-user-id를 읽으면 된다. Middleware에서 DB를 직접 조회하는 것은 지양하고, 가볍고 빠른 연산만 수행하는 것이 원칙이다.