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와 함께 사용하면 타입 안정성까지 보장받을 수 있어 더욱 견고한 웹 애플리케이션을 구축할 수 있습니다.
관련 게시글
React Server Components (RSC) 심층 가이드: Next.js App Router 활용
React Server Components(RSC)의 개념, 동작 원리, 장점, 그리고 Next.js App Router에서 RSC를 활용하는 방법을 심층적으로 탐구합니다. 프론트엔드 개발의 새로운 패러다임을 이해하고 실전 코드 예제를 통해 RSC를 마스터하세요.
Progressive Web App (PWA) 구축 실전: Next.js와 React를 활용한 PWA 개발 가이드
Next.js와 React 기반 PWA 구축 실전 가이드. Service Worker, Web App Manifest 설정 및 next-pwa 활용법을 통해 사용자 경험을 향상시키세요.
CSS Container Queries: 반응형 웹 디자인의 새로운 지평
CSS Container Queries를 활용하여 컴포넌트 기반 반응형 웹 디자인을 구현하는 방법을 심층적으로 알아봅니다. React, Next.js 환경에서 실전 예제를 통해 강력한 기능을 경험하세요.