Next.js 16 Proxy 마이그레이션: Node.js Runtime·matcher 검증

2026.07.16·수정 2026.07.19·약 19분

핵심 요약

Next.js 16에서 middleware.ts는 deprecated 되었고 파일 규약 이름이 proxy.ts로 바뀌었습니다. 그러나 단순한 파일명 교체로 끝나지 않습니다. Proxy는 Node.js runtime을 기본으로 사용하며 runtime을 설정할 수 없습니다. export const config = { runtime: 'edge' }를 Proxy에 넣으면 오류가 납니다. Edge runtime이 반드시 필요한 프로젝트만 Next.js 16 업그레이드 가이드의 임시 예외에 따라 middleware.ts를 유지하고, 나머지는 파일명·함수명·설정 플래그·matcher·테스트·배포 환경을 함께 이전해야 합니다.

검증 기준: 2026년 7월 19일, npm 최신 안정 버전 Next.js 16.2.10과 현재 Next.js 공식 문서를 기준으로 작성했습니다. 문서가 서로 다르게 보일 때는 현재 Proxy API 레퍼런스Next.js 16 업그레이드 가이드의 runtime 규칙을 우선 확인하세요.

먼저 Proxy가 필요한지 판단하기

Next.js 공식 가이드는 Proxy를 모든 서버 로직의 기본 위치로 권장하지 않습니다. 정적인 URL 이동은 먼저 next.config.tsredirects로 해결하고, 요청의 쿠키·헤더·경로를 읽어 렌더링 전에 동적으로 redirect, rewrite, header 변경을 해야 할 때 Proxy를 선택합니다. 느린 데이터 조회나 전체 세션 관리, 최종 권한 검증을 Proxy 하나에 몰아넣지 마세요.

요구사항 우선 선택 이유
고정된 이전 URL을 새 URL로 이동 next.config.ts redirects 요청별 로직 없이 더 단순하게 처리할 수 있습니다.
쿠키·헤더·경로에 따른 redirect/rewrite proxy.ts 라우트 렌더링 전에 요청 정보를 읽어 분기할 수 있습니다.
느린 API 조회와 캐시 재검증 Route Handler·Server Component Proxy의 fetch에서 cache, next.revalidate, next.tags는 효과가 없습니다.
최종 인증·인가 데이터 접근 계층·Server Function·Route Handler Proxy의 쿠키 검사는 빠른 optimistic check일 뿐 최종 보안 경계가 아닙니다.
Edge runtime을 반드시 유지 deprecated middleware.ts 임시 유지 Next.js 16의 proxy.ts는 Edge를 지원하지 않고 Node.js로 고정됩니다.

공식 Proxy 시작 가이드도 단순 redirect는 설정 파일을 먼저 고려하고, Proxy를 느린 데이터 fetching이나 완전한 세션 관리에 사용하지 말라고 안내합니다.

middleware에서 Proxy로 이름이 바뀐 이유

Next.js의 기존 Middleware는 Express.js middleware처럼 요청 처리 파이프라인의 범용 계층으로 오해되기 쉬웠습니다. 공식 문서는 이 기능을 앱 앞의 네트워크 경계에서 요청을 가로채는 역할로 더 분명하게 표현하고, 과도한 사용을 줄이기 위해 이름을 Proxy로 바꿨다고 설명합니다. Next.js 16.0.0부터 middleware 파일 규약은 deprecated이며 proxy가 새 이름입니다.

기능의 핵심은 여전히 라우트가 렌더링되기 전에 요청을 검사해 redirect, rewrite, 요청·응답 헤더 변경, 쿠키 처리 또는 직접 응답을 수행하는 것입니다. 프로젝트 루트 또는 src를 사용한다면 src 안에서 app 또는 pages와 같은 높이에 하나의 proxy.ts를 둡니다. 프로젝트마다 Proxy 파일은 하나만 지원하지만, 실제 로직은 여러 모듈로 분리해 가져올 수 있습니다.

Next.js 요청 흐름에서 Proxy가 라우트 렌더링 전에 실행되는 구조 다이어그램

Node.js runtime 고정과 Edge 예외

Next.js 16.2.10의 Proxy는 Node.js runtime이 기본이며 변경할 수 없습니다. Proxy 파일에서는 route segment의 runtime config를 사용할 수 없습니다. 다음 코드는 Edge로 바꾸는 설정이 아니라 빌드 오류를 만드는 잘못된 예입니다.

// proxy.ts - 잘못된 설정
export const config = {
  matcher: '/dashboard/:path*',
  runtime: 'edge', // Proxy에서는 사용할 수 없으며 오류가 납니다.
}

Edge runtime이 꼭 필요한 기존 프로젝트에는 예외가 있습니다. Next.js 16 공식 업그레이드 가이드는 Edge를 계속 써야 한다면 middleware.ts를 유지하라고 안내합니다. 이는 proxy.ts에 Edge 설정을 추가하라는 의미가 아니라, deprecated 규약을 임시로 유지하는 전환 경로입니다. Edge 의존성을 제거하기 전에는 codemod를 실행하지 말고 현재 16.2.10과 실제 배포 어댑터에서 동작을 별도로 검증하세요.

Runtime 결정 규칙

  • Node.js로 이동 가능: middleware.tsproxy.ts로 마이그레이션합니다.
  • Edge가 필수: middleware.ts를 임시 유지하고 Proxy와 동시에 두지 않습니다.
  • Proxy에서 runtime: 'nodejs'도 쓰지 않습니다. 기본값이므로 설정 자체가 불필요하고 허용되지 않습니다.
  • 파일명만 바꾼 뒤 런타임 차이를 무시하지 말고 import 패키지, 네트워크 호출, 플랫폼 제한을 다시 확인합니다.

지원 API와 지원하지 않는 사용 방식

구분 가능하거나 권장되는 범위 제약
요청 읽기 NextRequest, nextUrl, headers, cookies 민감한 쿠키 값을 응답 헤더나 로그로 노출하지 않습니다.
응답 제어 NextResponse.next, redirect, rewrite, 직접 Response 간단한 고정 redirect는 설정 파일을 먼저 고려합니다.
헤더·쿠키 업스트림 요청 헤더, 응답 헤더, 응답 쿠키 설정 업스트림용 헤더와 브라우저에 노출할 응답 헤더를 구분합니다.
백그라운드 작업 NextFetchEvent.waitUntil 요청 차단 로직이나 느린 핵심 처리를 숨기는 용도로 쓰지 않습니다.
runtime 설정 없음 runtime config는 사용할 수 없고 Proxy는 Node.js로 고정됩니다.
Edge Proxy에서는 미지원 Edge 필수 프로젝트는 deprecated middleware를 임시 유지합니다.
fetch 캐시 옵션 일반 fetch 호출 cache, next.revalidate, next.tags는 Proxy에서 효과가 없습니다.
배포 Node.js server, Docker, 플랫폼별 adapter static export는 Proxy를 지원하지 않으며 adapter 지원은 플랫폼별로 확인합니다.

Proxy가 Server Function이나 Route Handler의 최종 권한 검증을 대신할 수 없다는 점도 중요합니다. matcher에서 경로가 빠지거나 라우트가 이동하면 Proxy 검사가 조용히 우회될 수 있으므로, 데이터를 변경하거나 읽는 실제 서버 코드에서도 인증과 인가를 다시 검사해야 합니다.

안전한 마이그레이션 순서

  1. 현재 runtime 의존성을 확인합니다. Edge가 필수이면 여기서 중단하고 middleware를 임시 유지합니다.
  2. 기준 버전을 고정합니다. Next.js 16은 Node.js 20.9.0 이상이 필요하므로 로컬·CI·배포 이미지의 Node 버전을 맞춥니다.
  3. 현재 동작을 테스트로 고정합니다. 보호 경로, 공개 경로, redirect URL, rewrite, headers, cookies를 기록합니다.
  4. 공식 codemod를 실행합니다. 파일명, named export, 관련 config 이름의 변경 사항을 diff로 검토합니다.
  5. matcher를 다시 검토합니다. 정적 파일, 이미지, metadata 파일과 보호 경로가 의도대로 포함·제외되는지 테스트합니다.
  6. production build와 preview 배포를 검증합니다. 로컬 개발 서버만 통과했다고 완료하지 않습니다.
# 변경 내용을 먼저 커밋하라는 뜻이 아니라, 현재 작업 디렉터리 백업 후 실행합니다.
npx @next/codemod@latest middleware-to-proxy .

# codemod 후 확인할 대표 변경
# middleware.ts  -> proxy.ts
# export function middleware  -> export function proxy
# skipMiddlewareUrlNormalize  -> skipProxyUrlNormalize

현재 공식 Codemods 문서에 따르면 middleware-to-proxy는 파일명과 named export뿐 아니라 middleware 이름이 들어간 일부 Next.js config 속성도 proxy 이름으로 바꿉니다. 자동 변경을 그대로 신뢰하지 말고 프로젝트 고유 wrapper, import 경로, 테스트 이름, 배포 설정에 남은 middleware 문자열을 직접 검색하세요.

Next.js middleware에서 proxy로 마이그레이션할 때 확인할 체크리스트

matcher와 config 설계

matcher가 없으면 Proxy는 _next/static, _next/image, public asset을 포함한 모든 요청에서 실행됩니다. 인증 redirect가 CSS·JavaScript·이미지 요청까지 막지 않도록 가능한 한 보호할 경로를 직접 나열하는 방식부터 시작하세요.

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
}

matcher 값은 빌드 시 정적으로 분석할 수 있는 상수여야 합니다. 변수나 런타임 계산 값은 무시될 수 있습니다. 경로는 /로 시작하며 named parameter와 *, ?, + modifier, 정규식, 객체형 source, locale, has, missing 조건을 지원합니다.

광범위한 negative matcher가 꼭 필요하다면 api, _next/static, _next/image, favicon.ico, sitemap.xml, robots.txt를 제외하는 공식 예제를 출발점으로 삼되 실제 보안 경계를 테스트하세요. 공식 문서상 _next/data는 negative pattern에서 제외해도 보호 누락을 막기 위해 Proxy가 실행될 수 있습니다.

인증 리다이렉트 예제

다음 예제는 세션 쿠키가 없으면 로그인 화면으로 보내고, 있으면 내부 서버 코드가 참고할 요청 헤더를 추가합니다. 쿠키 존재 여부는 빠른 optimistic check일 뿐이며 실제 데이터 접근 시 세션 유효성과 권한을 다시 검증해야 합니다.

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

export function proxy(request: NextRequest) {
  const session = request.cookies.get('session')?.value

  if (!session) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set(
      'from',
      request.nextUrl.pathname + request.nextUrl.search,
    )
    return NextResponse.redirect(loginUrl)
  }

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-proxy-auth-checked', '1')

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

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
}

NextResponse.next({ request: { headers } })는 수정한 헤더를 업스트림 서버 코드에 전달합니다. NextResponse.next({ headers })처럼 최상위에 넣으면 클라이언트 응답 헤더로 노출될 수 있으므로 목적을 구분해야 합니다.

matcher와 Proxy 함수 테스트

Next.js는 next/experimental/testing/server에서 Proxy 단위 테스트 유틸리티를 제공합니다. 이름에 experimental이 있으므로 버전 업데이트 때 import와 동작을 다시 확인하세요. matcher 테스트와 실제 redirect 결과 테스트를 분리하면 경로 범위가 바뀌었을 때 원인을 빠르게 찾을 수 있습니다.

import { describe, expect, it } from 'vitest'
import { NextRequest } from 'next/server'
import {
  getRedirectUrl,
  unstable_doesProxyMatch,
} from 'next/experimental/testing/server'
import { config, proxy } from './proxy'

describe('proxy', () => {
  it('보호 경로만 matcher에 포함한다', () => {
    expect(
      unstable_doesProxyMatch({
        config,
        nextConfig: {},
        url: '/dashboard',
      }),
    ).toBe(true)

    expect(
      unstable_doesProxyMatch({
        config,
        nextConfig: {},
        url: '/about',
      }),
    ).toBe(false)
  })

  it('세션이 없으면 원래 경로를 포함해 로그인으로 보낸다', () => {
    const request = new NextRequest(
      'https://example.com/dashboard?tab=team',
    )
    const response = proxy(request)

    expect(getRedirectUrl(response)).toBe(
      'https://example.com/login?from=%2Fdashboard%3Ftab%3Dteam',
    )
  })
})

프로젝트가 Jest를 사용한다면 assertion import만 현재 테스트 환경에 맞추면 됩니다. 추가로 세션이 있는 요청, /settings 하위 경로, query string, trailing slash, public asset, prefetch request를 테스트하세요.

빌드·배포 검증

# 프로젝트의 실제 스크립트 이름에 맞춰 실행
npm run test
npm run build
npm run start

# 별도 터미널에서 redirect와 인증 통과 경로 확인
curl -I http://localhost:3000/dashboard
curl -I --cookie "session=test-session" http://localhost:3000/dashboard
  1. 환경: Next.js 16의 최소 Node.js 20.9.0 이상을 로컬, CI, 배포 이미지에서 동일하게 사용합니다.
  2. 빌드: runtime config가 남지 않았는지, matcher가 정적 상수인지, import가 서버 번들에서 해석되는지 확인합니다.
  3. 동작: 보호·공개 경로, redirect Location, rewrite, request/response headers, cookies를 production server에서 확인합니다.
  4. 플랫폼: Node.js server와 Docker는 지원되지만 static export는 지원되지 않습니다. adapter는 플랫폼별 지원 범위를 확인합니다.
  5. preview: 실제 배포 환경의 로그와 응답 헤더를 확인하고 redirect loop, 정적 자산 차단, 세션 누락을 점검합니다.
  6. 보안: Server Function과 Route Handler가 Proxy와 별개로 인증·인가를 검증하는지 확인합니다.

자주 하는 실수

실수 문제 교정
파일만 proxy.ts로 바꾸고 함수는 middleware로 둠 deprecated named export가 남습니다. named export를 proxy로 바꾸거나 default export를 사용합니다.
Proxy에 runtime: 'edge' 또는 'nodejs' 추가 Proxy는 runtime config를 허용하지 않습니다. runtime 설정을 제거합니다. Edge 필수이면 middleware를 임시 유지합니다.
matcher 없이 인증 redirect 실행 정적 파일과 이미지까지 redirect될 수 있습니다. 보호 경로를 명시하고 matcher 단위 테스트를 추가합니다.
동적 변수로 matcher 생성 빌드 시 정적 분석되지 않아 무시될 수 있습니다. 문자열·배열·객체를 리터럴 상수로 작성합니다.
Proxy 쿠키 검사만으로 최종 권한을 보장 matcher 변경이나 직접 서버 호출로 보안 공백이 생깁니다. 실제 데이터 접근 코드에서 인증·인가를 다시 검사합니다.
Proxy fetch에 revalidate나 tags 사용 해당 캐시 옵션은 Proxy에서 효과가 없습니다. 캐시가 필요한 조회는 적절한 서버 데이터 계층으로 옮깁니다.
output: 'export'와 함께 사용 static export는 Proxy를 지원하지 않습니다. Node.js server, Docker 또는 지원 adapter로 배포합니다.

최종 체크리스트

  • [ ] Edge runtime이 실제 필수인지 먼저 확인했습니다.
  • [ ] middleware.ts와 named export를 proxy.ts, proxy로 옮겼습니다.
  • [ ] Proxy의 runtime config를 완전히 제거했습니다.
  • [ ] middleware 이름이 포함된 Next.js config와 프로젝트 wrapper를 검색했습니다.
  • [ ] matcher를 정적 상수로 작성하고 보호·공개·정적 자산 경로를 테스트했습니다.
  • [ ] redirect, rewrite, headers, cookies의 기존 동작을 단위 테스트로 고정했습니다.
  • [ ] Server Function과 Route Handler에서 인증·인가를 다시 검사합니다.
  • [ ] Node.js 20.9.0 이상에서 test와 production build를 통과했습니다.
  • [ ] preview 배포에서 redirect loop와 자산 차단이 없는지 확인했습니다.
  • [ ] static export가 아니라 Proxy를 지원하는 배포 방식을 사용합니다.

공식 문서

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기