Next.js Middleware 완벽 가이드: 실전 활용법과 TypeScript 예제
Next.js 미들웨어의 핵심 개념부터 인증, 리다이렉트, 국제화까지 실전 TypeScript 코드 예제로 완벽 정리
Next.js Middleware 완벽 가이드: 실전 활용법과 TypeScript 예제
Next.js 13 이후 미들웨어는 웹 애플리케이션의 요청-응답 사이클에서 핵심적인 역할을 담당하고 있습니다. 인증, 리다이렉션, 국제화, 보안 헤더 설정 등 다양한 기능을 Edge Runtime에서 효율적으로 처리할 수 있어 현대 웹 개발에서 필수적인 도구가 되었습니다. 이 글에서는 Next.js 미들웨어의 핵심 개념부터 실전 활용법까지 TypeScript 예제와 함께 상세히 알아보겠습니다.
Next.js Middleware 기본 개념
Next.js 미들웨어는 요청이 완료되기 전에 실행되는 코드로, 프로젝트 루트의 middleware.ts 파일에 정의합니다. Edge Runtime에서 동작하여 빠른 응답 시간을 보장하며, 전역적으로 요청을 가로채어 처리할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 미들웨어 로직
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
미들웨어는 모든 라우트에서 실행되기 전에 동작하며, matcher 설정을 통해 특정 경로에서만 실행되도록 제한할 수 있습니다.
인증 및 권한 관리 구현
가장 일반적인 미들웨어 활용 사례는 사용자 인증과 권한 관리입니다. JWT 토큰을 검증하고 보호된 라우트에 대한 접근을 제어하는 예제를 살펴보겠습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET)
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 보호된 라우트 정의
const protectedRoutes = ['/dashboard', '/profile', '/admin']
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
)
if (isProtectedRoute) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
const { payload } = await jwtVerify(token, JWT_SECRET)
// 관리자 권한이 필요한 라우트 체크
if (pathname.startsWith('/admin') && payload.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// 사용자 정보를 헤더에 추가
const response = NextResponse.next()
response.headers.set('x-user-id', payload.sub as string)
response.headers.set('x-user-role', payload.role as string)
return response
} catch (error) {
// 토큰이 유효하지 않은 경우
const response = NextResponse.redirect(new URL('/login', request.url))
response.cookies.delete('auth-token')
return response
}
}
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|login|register).*)',
],
}
동적 리다이렉션과 URL 재작성
미들웨어를 활용하면 복잡한 리다이렉션 로직과 URL 재작성을 효율적으로 처리할 수 있습니다. A/B 테스트, 기능 플래그, 지역별 라우팅 등에 유용합니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname, search } = request.nextUrl
const url = request.nextUrl.clone()
// A/B 테스트 구현
if (pathname === '/product') {
const variant = request.cookies.get('ab-test-variant')?.value
if (!variant) {
// 50:50 분할 테스트
const newVariant = Math.random() < 0.5 ? 'A' : 'B'
const response = NextResponse.rewrite(
new URL(`/product-${newVariant}${search}`, request.url)
)
response.cookies.set('ab-test-variant', newVariant, {
maxAge: 60 * 60 * 24 * 30, // 30일
})
return response
}
return NextResponse.rewrite(
new URL(`/product-${variant}${search}`, request.url)
)
}
// 레거시 URL 리다이렉션
const redirectMap: Record<string, string> = {
'/old-about': '/about',
'/old-contact': '/contact',
'/blog/category': '/blog',
}
if (redirectMap[pathname]) {
return NextResponse.redirect(
new URL(redirectMap[pathname], request.url),
301
)
}
// 모바일 디바이스 감지 및 리다이렉션
const userAgent = request.headers.get('user-agent') || ''
const isMobile = /Mobile|Android|iPhone|iPad/.test(userAgent)
if (pathname === '/app' && isMobile) {
return NextResponse.redirect(new URL('/mobile-app', request.url))
}
return NextResponse.next()
}
국제화(i18n) 처리
다국어 지원 웹사이트에서 미들웨어는 사용자의 언어 선호도를 감지하고 적절한 로케일로 라우팅하는 데 활용됩니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['en', 'ko', 'ja', 'zh']
const defaultLocale = 'en'
function getLocale(request: NextRequest): string {
// 1. URL에서 로케일 확인
const pathname = request.nextUrl.pathname
const pathnameLocale = locales.find(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameLocale) return pathnameLocale
// 2. 쿠키에서 로케일 확인
const cookieLocale = request.cookies.get('locale')?.value
if (cookieLocale && locales.includes(cookieLocale)) {
return cookieLocale
}
// 3. Accept-Language 헤더에서 로케일 확인
const acceptLanguage = request.headers.get('accept-language')
if (acceptLanguage) {
const preferredLocale = acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim().substring(0, 2))
.find(lang => locales.includes(lang))
if (preferredLocale) return preferredLocale
}
return defaultLocale
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 정적 파일과 API 라우트는 제외
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
pathname.includes('.')
) {
return NextResponse.next()
}
// 이미 로케일이 포함된 경로인지 확인
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (!pathnameHasLocale) {
const locale = getLocale(request)
const newUrl = new URL(`/${locale}${pathname}`, request.url)
const response = NextResponse.redirect(newUrl)
response.cookies.set('locale', locale, { maxAge: 60 * 60 * 24 * 365 })
return response
}
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\..*|api).*)',
],
}보안 헤더와 CORS 설정
미들웨어를 통해 보안 헤더를 설정하고 CORS 정책을 구현하여 웹 애플리케이션의 보안을 강화할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// 보안 헤더 설정
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
)
// CORS 처리
const origin = request.headers.get('origin')
const allowedOrigins = [
'https://yourdomain.com',
'https://www.yourdomain.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean) as string[]
if (origin && allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin)
response.headers.set('Access-Control-Allow-Credentials', 'true')
}
// Preflight 요청 처리
if (request.method === 'OPTIONS') {
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
return new Response(null, { status: 200, headers: response.headers })
}
return response
}
로깅과 분석
미들웨어에서 요청 로깅과 분석 데이터를 수집하여 애플리케이션의 성능과 사용자 행동을 모니터링할 수 있습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
interface RequestLog {
timestamp: string
method: string
url: string
userAgent: string
ip: string
duration: number
}
async function logRequest(request: NextRequest, duration: number) {
const log: RequestLog = {
timestamp: new Date().toISOString(),
method: request.method,
url: request.url,
userAgent: request.headers.get('user-agent') || '',
ip: request.ip || request.headers.get('x-forwarded-for') || '',
duration,
}
// 로그를 외부 서비스로 전송 (예: 데이터베이스, 로깅 서비스)
if (process.env.NODE_ENV === 'production') {
try {
await fetch(process.env.LOGGING_ENDPOINT!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(log),
})
} catch (error) {
console.error('로깅 실패:', error)
}
} else {
console.log('Request Log:', log)
}
}
export async function middleware(request: NextRequest) {
const startTime = Date.now()
// 봇 트래픽 필터링
const userAgent = request.headers.get('user-agent') || ''
const isBot = /bot|crawler|spider|crawling/i.test(userAgent)
if (isBot && !userAgent.includes('Googlebot')) {
return new NextResponse('Forbidden', { status: 403 })
}
const response = NextResponse.next()
// 요청 ID 생성 및 헤더에 추가
const requestId = crypto.randomUUID()
response.headers.set('x-request-id', requestId)
// 비동기로 로깅 (응답 지연 방지)
const duration = Date.now() - startTime
logRequest(request, duration).catch(console.error)
return response
}
성능 최적화와 캐싱
미들웨어에서 캐싱 전략을 구현하고 성능을 최적화하는 방법을 알아보겠습니다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const response = NextResponse.next()
// 정적 리소스 캐싱
if (pathname.startsWith('/images/') || pathname.startsWith('/assets/')) {
response.headers.set(
'Cache-Control',
'public, max-age=31536000, immutable'
)
}
// API 응답 캐싱
if (pathname.startsWith('/api/')) {
const cacheKey = `${request.method}-${pathname}-${request.nextUrl.search}`
// 캐시 헤더 설정 (예: 5분 캐싱)
if (request.method === 'GET') {
response.headers.set(
'Cache-Control',
'public, s-maxage=300, stale-while-revalidate=86400'
)
response.headers.set('Vary', 'Accept-Encoding')
}
}
// 압축 힌트 제공
const acceptEncoding = request.headers.get('accept-encoding')
if (acceptEncoding?.includes('br')) {
response.headers.set('Content-Encoding', 'br')
} else if (acceptEncoding?.includes('gzip')) {
response.headers.set('Content-Encoding', 'gzip')
}
return response
}
마무리
Next.js 미들웨어는 웹 애플리케이션의 요청 처리 파이프라인에서 강력한 제어 기능을 제공합니다. 인증과 권한 관리부터 국제화, 보안 헤더 설정, 성능 최적화까지 다양한 용도로 활용할 수 있으며, Edge Runtime의 빠른 실행 속도 덕분에 사용자 경험을 해치지 않으면서도 복잡한 로직을 처리할 수 있습니다. TypeScript와 함께 사용하면 타입 안정성까지 보장받을 수 있어 더욱 견고한 웹 애플리케이션을 구축할 수 있습니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.