🎯 요약
Next.js Middleware는 요청과 응답 사이에서 실행되는 강력한 서버사이드 로직입니다. 인증 확인, 권한 관리, 동적 리다이렉트 등 웹 애플리케이션의 보안과 사용자 경험을 담당하는 핵심 기능이에요. 특히 프론트엔드 웹 개발에서 백엔드 로직을 효율적으로 처리할 수 있는 필수 도구입니다.
📋 목차
Next.js Middleware 기본 개념
📍 Middleware의 정의와 역할
Next.js Middleware는 요청이 완료되기 전에 실행되는 함수입니다. 루트 디렉토리에 middleware.ts
파일을 생성하면 모든 요청에 대해 이 로직이 먼저 실행되어요.
🚀 실무에서의 경험담
실무에서 Middleware를 도입하기 전에는 각 페이지마다 인증 로직을 반복해서 작성해야 했습니다. 특히 보호된 라우트가 많아질수록 코드 중복이 심각해지더라고요.
Middleware를 도입한 후에는 인증 로직을 중앙화할 수 있어서 유지보수가 훨씬 수월해졌습니다. 새로운 보호된 라우트를 추가할 때도 설정 파일 한 줄만 수정하면 되니까 개발 속도도 크게 향상되었어요.
Next.js Middleware 구현 5단계
Next.js Middleware는 라우트 레벨에서 요청을 가로채고 처리할 수 있는 강력한 서버사이드 기능입니다. 인증, 권한 관리, 리다이렉트 등을 효율적으로 처리할 수 있어요.
핵심 특징:
- 요청과 응답 사이에서 실행되는 서버사이드 로직
- 쿠키, 헤더, URL 조작 및 검증 가능
- NextResponse를 통한 리다이렉트, 리라이트 제어
- 경로별 매처 설정으로 선택적 적용
Next.js Middleware는 웹 애플리케이션의 관문 역할을 하는 핵심 기능입니다. 사용자가 페이지에 접근하기 전에 필요한 모든 검증과 처리를 담당하죠.
💡 왜 Middleware가 필요할까?
실제로 제가 개발하면서 겪었던 상황을 예로 들어보겠습니다:
// 기존 방식 (문제점이 많음)
// pages/dashboard.tsx
export default function Dashboard() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
// ❌ 각 페이지마다 인증 로직 반복
checkAuth().then(user => {
if (!user) {
router.push('/login');
return;
}
setUser(user);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>; // ❌ 로딩 화면 깜빡임
return <div>Dashboard Content</div>;
}
Middleware를 써야 하는 5가지 이유
- 중앙화된 인증: 모든 보호된 라우트를 한 곳에서 관리
- 성능 향상: 클라이언트 사이드 렌더링 전에 서버에서 처리
- 보안 강화: 클라이언트에서 우회할 수 없는 서버 레벨 검증
- 사용자 경험: 로딩 화면 없이 즉시 리다이렉트
- 코드 재사용: DRY 원칙에 따른 중복 코드 제거
기존 방식의 문제점:
- 각 페이지마다 인증 로직 중복 작성
- 클라이언트에서 처리하므로 보안 취약점 존재
- 로딩 상태 관리로 인한 UI 깜빡임
인증 시스템 구현 패턴
기본 인증 Middleware 구현
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { decrypt } from '@/lib/auth';
// 보호된 라우트와 공개 라우트 정의
const protectedRoutes = ['/dashboard', '/profile', '/admin'];
const publicRoutes = ['/login', '/signup', '/'];
const authRoutes = ['/login', '/signup'];
export default async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
const isProtectedRoute = protectedRoutes.some(route =>
path.startsWith(route)
);
const isPublicRoute = publicRoutes.includes(path);
const isAuthRoute = authRoutes.includes(path);
// 세션 쿠키에서 토큰 추출
const sessionCookie = request.cookies.get('session');
const session = sessionCookie ? await decrypt(sessionCookie.value) : null;
// 보호된 라우트 접근 시 인증 확인
if (isProtectedRoute && !session?.userId) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', path);
return NextResponse.redirect(loginUrl);
}
// 인증된 사용자가 로그인/회원가입 페이지 접근 시 대시보드로 리다이렉트
if (isAuthRoute && session?.userId) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
// API, _next/static, _next/image, favicon.ico 제외
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
세션 암호화/복호화 유틸리티
// lib/auth.ts
import 'server-only';
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';
const secretKey = process.env.SESSION_SECRET;
const key = new TextEncoder().encode(secretKey);
interface SessionPayload {
userId: string;
email: string;
role: string;
expiresAt: Date;
}
// 세션 생성 및 암호화
export async function encrypt(payload: SessionPayload) {
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(key);
}
// 세션 복호화 및 검증
export async function decrypt(session: string | undefined = '') {
try {
const { payload } = await jwtVerify(session, key, {
algorithms: ['HS256'],
});
return payload as SessionPayload;
} catch (error) {
console.log('Failed to verify session');
return null;
}
}
// 세션 생성
export async function createSession(userId: string, email: string, role: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7일
const session = await encrypt({ userId, email, role, expiresAt });
const cookieStore = await cookies();
cookieStore.set('session', session, {
expires: expiresAt,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
return session;
}
// 세션 삭제
export async function deleteSession() {
const cookieStore = await cookies();
cookieStore.delete('session');
}
// 세션 갱신
export async function updateSession(request: NextRequest) {
const session = request.cookies.get('session')?.value;
const payload = await decrypt(session);
if (!session || !payload) {
return null;
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const newSession = await encrypt({
...payload,
expiresAt: expires,
});
return NextResponse.next({
headers: {
'Set-Cookie': `session=${newSession}; Path=/; HttpOnly; SameSite=Lax; Expires=${expires.toUTCString()}`,
},
});
}
로그인 API 구현
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { createSession } from '@/lib/auth';
import { getUserByEmail } from '@/lib/db';
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
// 입력 검증
if (!email || !password) {
return NextResponse.json(
{ error: '이메일과 비밀번호를 입력해주세요.' },
{ status: 400 }
);
}
// 사용자 조회
const user = await getUserByEmail(email);
if (!user) {
return NextResponse.json(
{ error: '등록되지 않은 이메일입니다.' },
{ status: 401 }
);
}
// 비밀번호 검증
const isValidPassword = await bcrypt.compare(password, user.hashedPassword);
if (!isValidPassword) {
return NextResponse.json(
{ error: '비밀번호가 일치하지 않습니다.' },
{ status: 401 }
);
}
// 세션 생성
await createSession(user.id, user.email, user.role);
return NextResponse.json({
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
});
} catch (error) {
console.error('로그인 오류:', error);
return NextResponse.json(
{ error: '서버 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
권한 기반 접근 제어
역할별 라우트 보호
// middleware.ts - 권한별 접근 제어
import { NextRequest, NextResponse } from 'next/server';
import { decrypt } from '@/lib/auth';
// 권한별 라우트 매핑
const routePermissions = {
'/admin': ['admin'],
'/admin/users': ['admin'],
'/admin/settings': ['admin', 'super_admin'],
'/dashboard': ['user', 'admin', 'super_admin'],
'/dashboard/analytics': ['admin', 'super_admin'],
'/profile': ['user', 'admin', 'super_admin'],
} as const;
export default async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// 권한이 필요한 라우트 확인
const requiredRoles = Object.entries(routePermissions).find(([route]) =>
path.startsWith(route)
)?.[1];
if (!requiredRoles) {
return NextResponse.next();
}
// 세션 확인
const sessionCookie = request.cookies.get('session');
const session = sessionCookie ? await decrypt(sessionCookie.value) : null;
if (!session?.userId) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', path);
return NextResponse.redirect(loginUrl);
}
// 권한 확인
if (!requiredRoles.includes(session.role as any)) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
}
동적 권한 검증
// lib/permissions.ts
interface Permission {
resource: string;
action: 'read' | 'write' | 'delete' | 'admin';
conditions?: Record<string, any>;
}
interface UserSession {
userId: string;
role: string;
permissions: Permission[];
}
export function hasPermission(
session: UserSession,
resource: string,
action: string,
context?: Record<string, any>
): boolean {
// 슈퍼 관리자는 모든 권한
if (session.role === 'super_admin') {
return true;
}
// 명시적 권한 확인
const hasExplicitPermission = session.permissions.some(permission => {
if (permission.resource !== resource || permission.action !== action) {
return false;
}
// 조건부 권한 확인
if (permission.conditions && context) {
return Object.entries(permission.conditions).every(([key, value]) => {
return context[key] === value;
});
}
return true;
});
return hasExplicitPermission;
}
// 사용 예시
export async function checkResourceAccess(
request: NextRequest,
resource: string,
action: string
) {
const sessionCookie = request.cookies.get('session');
const session = sessionCookie ? await decrypt(sessionCookie.value) : null;
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
const userPermissions = await getUserPermissions(session.userId);
const hasAccess = hasPermission(
{ ...session, permissions: userPermissions },
resource,
action
);
if (!hasAccess) {
return NextResponse.json(
{ error: '접근 권한이 없습니다.' },
{ status: 403 }
);
}
return NextResponse.next();
}
동적 리다이렉트 전략
지능형 리다이렉트 시스템
// middleware.ts - 스마트 리다이렉트
import { NextRequest, NextResponse } from 'next/server';
export default async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
const session = await getSession(request);
// 사용자 상태별 리다이렉트 로직
if (!session) {
return handleUnauthenticatedUser(request, path);
}
return handleAuthenticatedUser(request, path, session);
}
function handleUnauthenticatedUser(request: NextRequest, path: string) {
// 공개 라우트는 그대로 진행
const publicRoutes = ['/login', '/signup', '/', '/about', '/contact'];
if (publicRoutes.includes(path) || path.startsWith('/public')) {
return NextResponse.next();
}
// 보호된 라우트는 로그인으로 리다이렉트 (return URL 포함)
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnUrl', path);
// 쿼리 파라미터도 함께 전달
if (request.nextUrl.search) {
loginUrl.searchParams.set('returnParams', request.nextUrl.search);
}
return NextResponse.redirect(loginUrl);
}
function handleAuthenticatedUser(
request: NextRequest,
path: string,
session: any
) {
// 첫 로그인 시 온보딩 플로우
if (session.isFirstLogin && !path.startsWith('/onboarding')) {
return NextResponse.redirect(new URL('/onboarding', request.url));
}
// 프로필 미완성 시 프로필 완성 페이지
if (!session.profileComplete && !path.startsWith('/complete-profile')) {
const excludePaths = ['/api', '/logout', '/profile'];
if (!excludePaths.some(excluded => path.startsWith(excluded))) {
return NextResponse.redirect(new URL('/complete-profile', request.url));
}
}
// 역할별 기본 대시보드 리다이렉트
if (path === '/' || path === '/dashboard') {
const dashboardMap = {
admin: '/admin/dashboard',
manager: '/manager/dashboard',
user: '/user/dashboard'
};
const targetDashboard = dashboardMap[session.role] || '/user/dashboard';
if (path !== targetDashboard) {
return NextResponse.redirect(new URL(targetDashboard, request.url));
}
}
return NextResponse.next();
}
A/B 테스트와 기능 플래그
// middleware.ts - 기능 플래그 기반 라우팅
import { NextRequest, NextResponse } from 'next/server';
interface FeatureFlag {
name: string;
enabled: boolean;
rolloutPercentage: number;
userSegments?: string[];
}
export default async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// 새 기능 라우트 체크
if (path.startsWith('/new-feature')) {
const canAccess = await checkFeatureAccess(request, 'new-feature-beta');
if (!canAccess) {
return NextResponse.redirect(new URL('/feature-not-available', request.url));
}
}
// A/B 테스트 라우팅
if (path === '/landing') {
const variant = await getABTestVariant(request, 'landing-page-test');
if (variant === 'B') {
return NextResponse.rewrite(new URL('/landing-variant-b', request.url));
}
}
return NextResponse.next();
}
async function checkFeatureAccess(
request: NextRequest,
featureName: string
): Promise<boolean> {
const session = await getSession(request);
const feature = await getFeatureFlag(featureName);
if (!feature.enabled) {
return false;
}
// 사용자 세그먼트 체크
if (feature.userSegments && session) {
const userSegments = await getUserSegments(session.userId);
const hasAccess = feature.userSegments.some(segment =>
userSegments.includes(segment)
);
if (hasAccess) {
return true;
}
}
// 롤아웃 퍼센테지 체크
const userId = session?.userId || request.ip || 'anonymous';
const hash = simpleHash(userId + featureName);
const percentage = hash % 100;
return percentage < feature.rolloutPercentage;
}
function simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 32비트 정수로 변환
}
return Math.abs(hash);
}
실무 보안 패턴과 최적화
보안 헤더 설정
// middleware.ts - 보안 헤더 적용
export default async 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(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
// CSP 헤더 (환경별 설정)
const cspHeader = process.env.NODE_ENV === 'production'
? "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
: "default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:;";
response.headers.set('Content-Security-Policy', cspHeader);
// HTTPS 강제 (프로덕션)
if (process.env.NODE_ENV === 'production' && request.nextUrl.protocol !== 'https:') {
return NextResponse.redirect(
`https://${request.nextUrl.host}${request.nextUrl.pathname}${request.nextUrl.search}`,
301
);
}
return response;
}
Rate Limiting 구현
// lib/rate-limit.ts
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
keyGenerator?: (request: NextRequest) => string;
}
class RateLimiter {
private requests = new Map<string, number[]>();
constructor(private config: RateLimitConfig) {}
async isAllowed(request: NextRequest): Promise<boolean> {
const key = this.config.keyGenerator?.(request) ||
request.ip ||
request.headers.get('x-forwarded-for') ||
'anonymous';
const now = Date.now();
const windowStart = now - this.config.windowMs;
// 현재 키의 요청 기록 가져오기
const requestTimes = this.requests.get(key) || [];
// 윈도우 범위 밖의 요청 제거
const validRequests = requestTimes.filter(time => time > windowStart);
// 요청 한도 확인
if (validRequests.length >= this.config.maxRequests) {
return false;
}
// 새 요청 기록
validRequests.push(now);
this.requests.set(key, validRequests);
// 정리 (메모리 누수 방지)
this.cleanup();
return true;
}
private cleanup() {
const now = Date.now();
for (const [key, requests] of this.requests.entries()) {
const validRequests = requests.filter(time =>
time > now - this.config.windowMs
);
if (validRequests.length === 0) {
this.requests.delete(key);
} else {
this.requests.set(key, validRequests);
}
}
}
}
// 사용 예시
const apiRateLimit = new RateLimiter({
windowMs: 15 * 60 * 1000, // 15분
maxRequests: 100,
});
const authRateLimit = new RateLimiter({
windowMs: 15 * 60 * 1000, // 15분
maxRequests: 5, // 로그인 시도 제한
keyGenerator: (request) => `auth:${request.ip}`,
});
export async function checkRateLimit(
request: NextRequest,
limiter: RateLimiter
): Promise<NextResponse | null> {
const allowed = await limiter.isAllowed(request);
if (!allowed) {
return NextResponse.json(
{
error: '요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.',
retryAfter: Math.ceil(limiter.config.windowMs / 1000)
},
{
status: 429,
headers: {
'Retry-After': Math.ceil(limiter.config.windowMs / 1000).toString(),
}
}
);
}
return null;
}
실전 Middleware 통합 예제
// middleware.ts - 종합 실무 버전
import { NextRequest, NextResponse } from 'next/server';
import { decrypt, updateSession } from '@/lib/auth';
import { checkRateLimit, apiRateLimit, authRateLimit } from '@/lib/rate-limit';
export default async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// 1. Rate Limiting
if (path.startsWith('/api/auth')) {
const rateLimitResponse = await checkRateLimit(request, authRateLimit);
if (rateLimitResponse) return rateLimitResponse;
} else if (path.startsWith('/api/')) {
const rateLimitResponse = await checkRateLimit(request, apiRateLimit);
if (rateLimitResponse) return rateLimitResponse;
}
// 2. 보안 헤더 설정
const response = await handleAuthentication(request);
setSecurityHeaders(response);
return response;
}
async function handleAuthentication(request: NextRequest) {
const path = request.nextUrl.pathname;
// 정적 파일과 API는 인증 체크 제외
if (path.startsWith('/_next') || path.startsWith('/api/')) {
return NextResponse.next();
}
const sessionCookie = request.cookies.get('session');
const session = sessionCookie ? await decrypt(sessionCookie.value) : null;
// 보호된 라우트 정의
const protectedRoutes = ['/dashboard', '/profile', '/admin'];
const isProtectedRoute = protectedRoutes.some(route => path.startsWith(route));
if (isProtectedRoute && !session?.userId) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnUrl', path);
return NextResponse.redirect(loginUrl);
}
// 권한 확인
if (session && isProtectedRoute) {
const hasPermission = await checkRoutePermission(path, session);
if (!hasPermission) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
}
// 세션 갱신
if (session) {
return await updateSession(request);
}
return NextResponse.next();
}
function setSecurityHeaders(response: NextResponse) {
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');
return response;
}
async function checkRoutePermission(path: string, session: any): Promise<boolean> {
// 관리자 라우트는 admin 역할만 접근 가능
if (path.startsWith('/admin')) {
return session.role === 'admin' || session.role === 'super_admin';
}
// 일반 보호된 라우트는 인증된 사용자 모두 접근 가능
return true;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
};
성능 최적화와 디버깅
Middleware 성능 모니터링
// lib/middleware-performance.ts
interface PerformanceMetrics {
executionTime: number;
memoryUsage: number;
route: string;
timestamp: number;
}
class MiddlewarePerformanceMonitor {
private metrics: PerformanceMetrics[] = [];
startMeasure(route: string) {
return {
route,
startTime: performance.now(),
startMemory: process.memoryUsage().heapUsed,
};
}
endMeasure(measurement: ReturnType<typeof this.startMeasure>) {
const executionTime = performance.now() - measurement.startTime;
const memoryUsage = process.memoryUsage().heapUsed - measurement.startMemory;
const metric: PerformanceMetrics = {
executionTime,
memoryUsage,
route: measurement.route,
timestamp: Date.now(),
};
this.metrics.push(metric);
// 성능 임계값 경고
if (executionTime > 100) { // 100ms 이상
console.warn(`Middleware 성능 경고: ${measurement.route} - ${executionTime.toFixed(2)}ms`);
}
// 메트릭 저장소 크기 제한
if (this.metrics.length > 1000) {
this.metrics = this.metrics.slice(-500);
}
return metric;
}
getAverageExecutionTime(route?: string): number {
const relevantMetrics = route
? this.metrics.filter(m => m.route === route)
: this.metrics;
if (relevantMetrics.length === 0) return 0;
const total = relevantMetrics.reduce((sum, m) => sum + m.executionTime, 0);
return total / relevantMetrics.length;
}
getPerformanceReport() {
const routes = [...new Set(this.metrics.map(m => m.route))];
return routes.map(route => ({
route,
avgExecutionTime: this.getAverageExecutionTime(route),
requestCount: this.metrics.filter(m => m.route === route).length,
maxExecutionTime: Math.max(...this.metrics
.filter(m => m.route === route)
.map(m => m.executionTime)
),
}));
}
}
export const performanceMonitor = new MiddlewarePerformanceMonitor();
// 사용 예시
export function withPerformanceMonitoring(
handler: (request: NextRequest) => Promise<NextResponse>
) {
return async (request: NextRequest) => {
const measurement = performanceMonitor.startMeasure(request.nextUrl.pathname);
try {
const response = await handler(request);
performanceMonitor.endMeasure(measurement);
return response;
} catch (error) {
performanceMonitor.endMeasure(measurement);
throw error;
}
};
}
디버깅과 로깅
// lib/middleware-logger.ts
interface LogEntry {
timestamp: string;
level: 'info' | 'warn' | 'error';
route: string;
action: string;
userId?: string;
details?: Record<string, any>;
}
class MiddlewareLogger {
private logs: LogEntry[] = [];
private createLogEntry(
level: LogEntry['level'],
route: string,
action: string,
details?: Record<string, any>,
userId?: string
): LogEntry {
return {
timestamp: new Date().toISOString(),
level,
route,
action,
userId,
details,
};
}
info(route: string, action: string, details?: Record<string, any>, userId?: string) {
const entry = this.createLogEntry('info', route, action, details, userId);
this.logs.push(entry);
if (process.env.NODE_ENV === 'development') {
console.log(`[Middleware] ${entry.timestamp} - ${action} on ${route}`, details);
}
}
warn(route: string, action: string, details?: Record<string, any>, userId?: string) {
const entry = this.createLogEntry('warn', route, action, details, userId);
this.logs.push(entry);
console.warn(`[Middleware Warning] ${entry.timestamp} - ${action} on ${route}`, details);
}
error(route: string, action: string, error: Error, userId?: string) {
const entry = this.createLogEntry('error', route, action, {
message: error.message,
stack: error.stack,
}, userId);
this.logs.push(entry);
console.error(`[Middleware Error] ${entry.timestamp} - ${action} on ${route}`, error);
}
getRecentLogs(count: number = 50): LogEntry[] {
return this.logs.slice(-count);
}
getLogsByRoute(route: string): LogEntry[] {
return this.logs.filter(log => log.route === route);
}
clearLogs() {
this.logs = [];
}
}
export const middlewareLogger = new MiddlewareLogger();
// 사용 예시
export function withLogging(
handler: (request: NextRequest) => Promise<NextResponse>
) {
return async (request: NextRequest) => {
const route = request.nextUrl.pathname;
const startTime = Date.now();
try {
middlewareLogger.info(route, 'Request started', {
method: request.method,
userAgent: request.headers.get('user-agent'),
});
const response = await handler(request);
const duration = Date.now() - startTime;
middlewareLogger.info(route, 'Request completed', {
status: response.status,
duration: `${duration}ms`,
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
middlewareLogger.error(route, 'Request failed', error as Error);
throw error;
}
};
}
자주 묻는 질문 (FAQ)
Q1: Middleware에서 데이터베이스에 직접 접근해도 되나요?
A: 가능하지만 성능상 권장하지 않습니다. 세션 정보는 JWT나 캐시를 활용하고, 꼭 필요한 경우에만 DB 접근을 최소화하세요.
Q2: Middleware 실행 순서를 제어할 수 있나요?
A: Next.js는 단일 middleware.ts 파일만 지원합니다. 여러 로직이 필요하면 함수로 분리해서 순서대로 호출하는 방식을 사용하세요.
Q3: Middleware에서 redirect와 rewrite의 차이점은 무엇인가요?
A: redirect는 브라우저 URL이 변경되고, rewrite는 URL은 그대로 두고 내부적으로 다른 페이지를 보여줍니다. SEO와 사용자 경험을 고려해서 선택하세요.
Q4: Middleware 성능이 느려지면 어떻게 최적화하나요?
A: 불필요한 로직 제거, 캐싱 활용, 조건문 순서 최적화, 그리고 matcher 설정으로 실행 범위를 제한하는 것이 효과적입니다.
Q5: 개발 환경에서 Middleware 디버깅이 어려운데 해결법이 있나요?
A: console.log를 적극 활용하고, 성능 모니터링 도구를 도입하세요. 로그 레벨을 나누어 환경별로 다르게 출력하는 것도 도움이 됩니다.
❓ Next.js Middleware 마스터 마무리
Next.js Middleware는 웹 애플리케이션의 보안과 사용자 경험을 한 단계 끌어올리는 핵심 도구입니다. 인증부터 권한 관리, 스마트 리다이렉트까지 서버 레벨에서 효율적으로 처리할 수 있어요.
특히 프론트엔드 웹 개발에서 백엔드 로직을 통합적으로 관리할 수 있는 강력한 기능이니까, 실무에서 이런 패턴들을 활용해보시면 개발 생산성과 보안성을 동시에 확보할 수 있을 거예요!
Next.js 고급 기법 더 배우고 싶다면 Next.js 배포와 최적화 전략과 Next.js API Routes 마스터를 꼭 확인해보세요! 💪
🔗 Next.js Middleware 심화 학습 시리즈
Next.js Middleware 마스터가 되셨다면, 다른 고급 기능들도 함께 학습해보세요:
📚 다음 단계 학습 가이드
- Next.js 15 완전 정복 가이드: React 19와 Turbopack으로 극한 성능 최적화
- Next.js 배포와 최적화 전략: Vercel vs AWS 비교 분석과 성능 튜닝 실전
- Next.js API Routes 마스터: 실무에서 3년간 써본 완전 정복 가이드
- Next.js + Prisma 실무 개발: 데이터베이스 연동과 ORM 최적화 완전정복
- Next.js SEO 최적화 가이드: 메타데이터 관리와 사이트맵 자동화 전략