Logo

Next.js Middleware 활용법: 인증/권한/리다이렉트 로직 구현 마스터

🎯 요약

Next.js Middleware는 요청과 응답 사이에서 실행되는 강력한 서버사이드 로직입니다. 인증 확인, 권한 관리, 동적 리다이렉트 등 웹 애플리케이션의 보안과 사용자 경험을 담당하는 핵심 기능이에요. 특히 프론트엔드 웹 개발에서 백엔드 로직을 효율적으로 처리할 수 있는 필수 도구입니다.

📋 목차

  1. Next.js Middleware 기본 개념
  2. 인증 시스템 구현 패턴
  3. 권한 기반 접근 제어
  4. 동적 리다이렉트 전략
  5. 실무 보안 패턴과 최적화
  6. 성능 최적화와 디버깅

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가지 이유

  1. 중앙화된 인증: 모든 보호된 라우트를 한 곳에서 관리
  2. 성능 향상: 클라이언트 사이드 렌더링 전에 서버에서 처리
  3. 보안 강화: 클라이언트에서 우회할 수 없는 서버 레벨 검증
  4. 사용자 경험: 로딩 화면 없이 즉시 리다이렉트
  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 마스터가 되셨다면, 다른 고급 기능들도 함께 학습해보세요:

📚 다음 단계 학습 가이드

📚 공식 문서 및 참고 자료