기술 포스트

Middleware로 할 수 있는 것과 하면 안 되는 것


Article

이 글은 Next.js 15 이전 기준이다.

Next.js 16에서 middleware.ts는 deprecated 됐고 **proxy.ts**로 이름이 바뀌었다. 내보내는 함수 이름도 proxy이고, 기본 런타임이 Edge가 아니라 Node.js다. 아래에서 "Edge Runtime의 벽"이라 부른 제약 중 일부는 이제 해당하지 않는다. 옛 코드는 npx @next/codemod@canary middleware-to-proxy . 로 옮긴다.

최신 기준으로 정리한 강의는 이쪽이다 — proxy.ts 와 라우트 핸들러, 그리고 배포

다만 이 글의 결론은 지금도 유효하다. 거기에 너무 많은 걸 넣지 말라는 것. 공식 문서도 이 기능을 최후의 수단으로만 쓰라고 권한다.

Middleware에 너무 많은 걸 넣었다

Next.js의 Middleware를 처음 발견했을 때, 만능 도구처럼 보였다.

모든 요청을 가로챌 수 있다. 리다이렉트할 수 있다. 헤더를 수정할 수 있다. "그러면 인증도 여기서 하고, 로깅도 여기서 하고, 데이터 검증도 여기서 하면 되겠네?"

그렇게 Middleware에 로직을 몰아넣었다가 Edge Runtime의 벽에 부딪혔다.


Middleware의 기본 동작

Middleware는 middleware.ts 파일 하나로 동작한다. 프로젝트 루트(또는 src/)에 위치한다.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  console.log('요청:', request.nextUrl.pathname);
  return NextResponse.next();
}

// 어떤 경로에서 실행할지 지정
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

실행 시점: 요청이 들어올 때, 라우트 핸들러나 페이지가 실행되기 전에 실행된다.

실행 환경: Edge Runtime. 이게 핵심이다. 일반 Node.js가 아니다.


할 수 있는 것들

1. 인증 체크 & 리다이렉트

가장 흔한 사용 사례다.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  const { pathname } = request.nextUrl;

  // 인증이 필요한 경로
  if (pathname.startsWith('/dashboard')) {
    if (!token) {
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('from', pathname);
      return NextResponse.redirect(loginUrl);
    }
  }

  // 이미 로그인한 사용자가 로그인 페이지 접근 시
  if (pathname === '/login' && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

포인트: 토큰의 존재 여부만 확인한다. 토큰의 유효성 검증(DB 조회 등)은 여기서 하지 않는다. 그건 Server Component나 API Route에서 한다.

2. 리다이렉트 & 리라이트

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 오래된 URL 리다이렉트
  if (pathname === '/old-blog') {
    return NextResponse.redirect(new URL('/blog', request.url));
  }

  // URL은 유지하면서 다른 페이지 렌더링 (리라이트)
  if (pathname === '/docs') {
    return NextResponse.rewrite(new URL('/documentation/latest', request.url));
  }

  return NextResponse.next();
}

3. 헤더 추가/수정

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 보안 헤더 추가
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');

  // 요청 헤더에 정보 추가 (Server Component에서 읽기 위해)
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-pathname', request.nextUrl.pathname);

  return NextResponse.next({
    request: { headers: requestHeaders },
  });
}

4. i18n 라우팅

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 이미 로케일이 있으면 패스
  if (pathname.startsWith('/ko') || pathname.startsWith('/en')) {
    return NextResponse.next();
  }

  // Accept-Language 헤더에서 선호 언어 감지
  const locale = request.headers.get('accept-language')?.includes('ko')
    ? 'ko'
    : 'en';

  return NextResponse.redirect(
    new URL(`/${locale}${pathname}`, request.url)
  );
}

5. A/B 테스트

export function middleware(request: NextRequest) {
  const bucket = request.cookies.get('ab-bucket')?.value;
  const response = NextResponse.next();

  if (!bucket) {
    // 50/50 분배
    const newBucket = Math.random() < 0.5 ? 'A' : 'B';
    response.cookies.set('ab-bucket', newBucket);
  }

  return response;
}

하면 안 되는 것들

1. 무거운 인증 검증

// ❌ Middleware에서 하면 안 되는 것
export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  // DB 조회 → Edge Runtime에서 대부분의 ORM이 작동하지 않음
  const user = await prisma.user.findUnique({ where: { token } });

  // JWT 검증 → 일부 crypto API가 Edge에서 지원 안 됨
  const decoded = jwt.verify(token, process.env.SECRET);
}

왜 안 되는가:

  • Edge Runtime은 Node.js API의 일부만 지원한다
  • fs, net, child_process 등 사용 불가
  • 대부분의 ORM(Prisma, TypeORM 등)이 작동하지 않는다
  • 일부 crypto 함수가 없다

대안: Middleware에서는 토큰 존재 여부만 확인하고, 상세 검증은 Server Component에서:

// middleware.ts — 가볍게
export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

// app/(protected)/layout.tsx — 상세 검증
export default async function ProtectedLayout({ children }) {
  const session = await verifySession(); // DB 조회, JWT 검증 등
  if (!session) redirect('/login');
  return <>{children}</>;
}

2. 데이터 가져오기 (fetch 남용)

// ❌ Middleware에서 외부 API 호출
export async function middleware(request: NextRequest) {
  const config = await fetch('https://api.example.com/config');
  const features = await fetch('https://api.example.com/features');
  // ...
}

왜 안 되는가:

  • 모든 요청마다 실행된다. 이미지, CSS, JS 요청에도.
  • 외부 API가 느리면 전체 사이트가 느려진다
  • Middleware에 matcher를 설정해도, 불필요한 fetch는 성능을 잡아먹는다

3. 복잡한 비즈니스 로직

// ❌ Middleware에서 비즈니스 로직 처리
export async function middleware(request: NextRequest) {
  const user = getUserFromToken(request);

  // 구독 상태 확인
  if (user.subscription === 'free' && request.nextUrl.pathname.startsWith('/premium')) {
    return NextResponse.redirect(new URL('/upgrade', request.url));
  }

  // 사용량 제한
  if (user.apiCalls > user.limit) {
    return NextResponse.json({ error: 'Rate limited' }, { status: 429 });
  }
}

이런 로직은 Server Component나 Route Handler에서 처리해야 한다. Middleware는 라우팅 수준의 결정만 해야 한다.


역할 분담 정리

역할 Middleware Server Component Route Handler
토큰 존재 확인 - -
토큰 유효성 검증
리다이렉트 ✅ (redirect)
헤더 추가 -
DB 조회
외부 API 호출 ⚠️ (최소한)
쿠키 설정
i18n 감지 - -
비즈니스 로직

matcher 설정 팁

config.matcher를 제대로 설정하지 않으면, 모든 요청에 Middleware가 실행된다. 정적 파일 요청 포함.

export const config = {
  matcher: [
    // 정적 파일과 내부 Next.js 경로 제외
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

또는 필요한 경로만 명시적으로 지정:

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*', '/login', '/signup'],
};

팁: 명시적으로 지정하는 게 디버깅하기 쉽다. "왜 이 요청에 Middleware가 실행되지?"라는 질문을 줄여준다.


정리

Middleware는 문지기다. 누가 들어올 수 있는지 빠르게 확인하고, 방향을 안내하는 역할이다.

문지기가 할 일:

  • 출입증(토큰) 있는지 확인
  • 올바른 방향(리다이렉트/리라이트)으로 안내
  • 방문자 표시(헤더/쿠키) 부착

문지기가 하면 안 되는 일:

  • 출입증의 진위 여부를 감별 (그건 보안실에서)
  • 방문자의 업무를 대신 처리 (그건 담당 부서에서)
  • 모든 방문자의 신상을 DB에서 조회 (그건 행정실에서)

Middleware의 경계를 아는 것, 그게 실력이다.

Middleware로 할 수 있는 것과 하면 안 되는 것 — 디코드랩(DCODELAB)