Next.js 16으로 올린 다른 프로젝트에서 middleware.ts를 그대로 두고 빌드를 돌렸더니 deprecation 경고가 뜨면서 새 이름을 쓰라고 했습니다. 단순한 이름 변경처럼 보였는데, 공식 가이드를 읽고 나니 함께 따라오는 변화가 둘 더 있었습니다. 하나는 proxy에 더는 edge runtime이 없다는 것, 다른 하나는 Server Function이 proxy matcher 밖에 있지 않다는 것이었습니다. 두 가지 모두 빌드는 통과시키면서 운영에서 문제가 드러나는 종류의 변화라, 마이그레이션 시 한 번씩 손으로 짚어 두는 게 좋습니다.
이름이 왜 proxy가 됐나#
업그레이드 가이드는 이유를 짧게 적습니다.
The
middlewarefilename is deprecated, and has been renamed toproxyto clarify network boundary and routing focus.
용어 정리에 가깝습니다. 기존 middleware라는 이름은 Express 미들웨어처럼 요청·응답 파이프라인의 한 단계 같은 인상이지만, Next.js의 그것은 실제로는 라우트가 결정되기 전 네트워크 경계에서 동작하는 reverse proxy에 가깝습니다. CDN에 떨어져 fast redirect/rewrite를 처리할 수 있게 설계된 자리라, proxy.ts라는 이름이 의도와 잘 맞습니다.
문법적으로 바뀌어야 하는 부분은 세 군데입니다.
// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/admin")) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/admin/:path*"],
};
파일 이름을 proxy.ts로, named export 이름을 proxy로 바꿉니다. next.config.ts의 플래그도 따라가서, skipMiddlewareUrlNormalize는 skipProxyUrlNormalize로 바뀝니다. 여기까지는 npx @next/codemod@canary upgrade latest가 알아서 해 줍니다.
edge runtime은 함께 옮겨오지 않았다#
여기까지만 보면 단순한 리네이밍입니다. 진짜 변화는 런타임에 있습니다. proxy 공식 reference는 한 줄로 못을 박습니다.
Proxy defaults to using the Node.js runtime. The
runtimeconfig option is not available in Proxy files. Setting theruntimeconfig option in Proxy will throw an error.
업그레이드 가이드도 같은 톤입니다.
The
edgeruntime is NOT supported inproxy. Theproxyruntime isnodejs, and it cannot be configured. If you want to continue using theedgeruntime, keep usingmiddleware.
영향을 받는 시나리오가 분명합니다. 기존 middleware에 export const config = { runtime: "edge" }를 적어 edge에서 돌리던 코드를 proxy.ts로 옮기면, 그 라인은 더 이상 유효하지 않습니다. Cloudflare Workers나 Vercel Edge에서 인증·지오 라우팅·A/B 분기를 처리해 온 프로젝트가 정면으로 부딪치는 지점입니다. v16은 두 가지 선택지를 둡니다.
- proxy로 이름만 바꾸고 Node.js 런타임에서 동작시킨다. 라우트 결정 전이라는 성격은 그대로지만, edge가 주던 cold start 이점과 글로벌 분산은 잃습니다.
- 이름과 컨벤션을 모두
middleware로 유지한다. v16 시점에서 edge runtime을 쓰려면 deprecated된middleware컨벤션을 의도적으로 유지해야 합니다. 가이드는 이후 minor 릴리스에서 edge runtime instructions를 따로 정리하겠다고 약속해 두었습니다.
정적 export 환경이라면 애초에 두 컨벤션 모두 의미가 없습니다. 이 사이트처럼 output: "export"로 빌드되는 정적 사이트에서는 proxy/middleware 자체가 빌드 산출물에 들어오지 않습니다. 같은 결의 정적 export 함정은 정적 export에서 dynamicParams=false로 404 동작을 일관되게 만든 이야기에 정리해 두었습니다. 정적 export 사이트가 인증·리다이렉트를 처리해야 한다면 그 책임이 호스팅 측 redirect 규칙(예: Cloudflare Pages _redirects)이나 클라이언트 측 라우트 가드로 옮겨갑니다.
Server Function은 proxy matcher 밖이 아니다#
조용한 보안 함정 하나가 같은 reference에 적혀 있습니다. proxy의 실행 순서와 matcher 동작을 설명하면서 v16 docs가 새로 강조한 문단입니다.
Server Functions are not separate routes in this chain. They are handled as POST requests to the route where they are used, so a Proxy matcher that excludes a path will also skip Server Function calls on that path.
A matcher change or a refactor that moves a Server Function to a different route can silently remove Proxy coverage. Always verify authentication and authorization inside each Server Function rather than relying on Proxy alone.
핵심은 두 가지입니다. 하나, Server Function 호출은 별도의 라우트 엔트리가 아니라, 그 함수를 사용하는 라우트로 가는 POST 요청으로 처리됩니다. 둘, 따라서 proxy matcher가 그 라우트를 제외하면, 같은 페이지에서 호출되는 Server Function 역시 proxy 인증 검사를 우회합니다.
흔히 만드는 실수 패턴을 코드로 보면 이렇습니다.
// proxy.ts
export const config = {
matcher: ["/dashboard/:path*"],
};
export function proxy(request: NextRequest) {
const session = request.cookies.get("session");
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
이 matcher는 /dashboard/*에 들어오는 페이지 요청에 대해서만 동작합니다. 만약 dashboard에 마운트되어 있던 deleteAccount Server Function을 누군가 /settings 페이지로 옮기면, 그 함수의 POST 요청 경로가 /dashboard/* matcher 바깥으로 빠집니다. proxy는 호출되지 않고, 인증 없이 함수가 실행됩니다. 라우트 이동 하나만으로 인증 한 겹이 조용히 사라지는 셈입니다.
방어선은 proxy matcher가 아니라 함수 본체여야 합니다.
// app/settings/actions.ts
"use server";
import { auth } from "@/lib/auth";
export async function deleteAccount() {
const session = await auth();
if (!session) {
throw new Error("Unauthorized");
}
// 실제 삭제 로직
}
매 Server Function 진입점에서 세션을 확인하면, 라우트가 어디로 이동하든 인증 한 겹은 유지됩니다. proxy는 fast redirect를 위한 네트워크 경계 라우팅이고, 함수 단위 권한 검사는 데이터 보안 가이드가 권하는 패턴 그대로 함수 안에 두는 게 안전합니다.
한 줄 요약: 리네이밍이 아니라 정리다#
middleware → proxy는 단어 하나 갈아 끼우는 변화처럼 보이지만, edge runtime의 거주지가 바뀌었고, Server Function의 보안 책임이 proxy 밖으로 명시적으로 끌려 나왔습니다. 마이그레이션 체크리스트를 만들 때 세 줄만 기억해 두면 깔끔합니다. 이름과 named export, config 플래그를 proxy로 바꾼다. edge runtime이 필요한 코드는 middleware로 남기거나 호스팅 레이어로 옮긴다. 인증·권한 검사는 모든 Server Function 본체에 두어 matcher 변경에 묶이지 않게 한다.