Next.js 16 Proxy 마이그레이션
이런 경험 있으신가요?
Next.js 16으로 업그레이드하고 나서 개발 서버를 실행했는데, 터미널에 이런 경고가 뜨는 경우를 겪어보셨나요?
⚠ The "middleware" file convention is deprecated.
Please use "proxy" instead.
처음에는 단순한 경고라고 생각했지만, 곧바로 두 번째 에러가 나타났어요.
⨯ The file "./proxy.ts" must export a function,
either as a default export or as a named "proxy" export.
파일명만 바꾸면 되겠지? 생각하고 middleware.ts를 proxy.ts로 리네임했는데, 여전히 에러가 발생했습니다. Next.js 16에서는 파일명뿐만 아니라 함수명까지 바뀌어야 한다는 걸 그때 알게 되었습니다.
middleware.ts vs proxy.ts, 무엇이 바뀌었나요?
Next.js 16은 단순한 이름 변경이 아니라, 개념적인 변화를 담고 있습니다.
middleware.ts: “미들웨어 패턴”
// middleware.ts (Next.js 15 이하)
export function middleware(request: NextRequest) {
// 요청 처리
return NextResponse.next();
}
export const config = {
matcher: '/:path*',
};
middleware는 Express.js에서 빌려온 개념입니다. 생각해보면, 모든 페이지에서 공통으로 처리해야 하는 로직(HTTPS 리다이렉트, 로그인 체크, 언어 설정 등)을 매번 반복해서 작성하는 건 비효율적이죠. 그래서 요청이 실제 페이지에 도달하기 전에 먼저 가로채서 처리하는 기능이 필요합니다.
예를 들어, 100개의 페이지가 있다면:
❌ 미들웨어 없이: 100개 페이지 모두에 로그인 체크 코드 작성
✅ 미들웨어 사용: 한 파일에만 작성하면 모든 페이지에 자동 적용
proxy.ts: “프록시 패턴”
// proxy.ts (Next.js 16 이상)
export function proxy(request: NextRequest) {
// 요청 처리
return NextResponse.next();
}
export const config = {
matcher: '/:path*',
};
proxy는 더 명확한 이름입니다. 이 기능이 실제로 하는 일을 생각해보면:
- HTTP 요청을 HTTPS로 바꿔서 전달 (클라이언트는 모름)
- 로그인이 안 된 사용자를 로그인 페이지로 보내기 (원래 페이지는 실행 안 됨)
- 사용자 언어에 따라 /ko/about 또는 /en/about으로 분기
- 특정 IP는 차단하고, 나머지는 통과
이런 동작들은 클라이언트와 서버 사이에서 중간에서 요청을 받아 처리하고 전달하는 프록시의 역할과 정확히 일치합니다.
왜 이름을 바꿨을까요?
“미들웨어”는 너무 포괄적입니다. 데이터베이스 미들웨어, 인증 미들웨어, 로깅 미들웨어… 모든 게 미들웨어죠. 하지만 “프록시”는 정확합니다. 클라이언트 대신 요청을 받아서, 검사하고, 수정하고, 최종 목적지로 전달하는 것. 바로 Next.js의 이 기능이 하는 일입니다.
핵심 차이점 한눈에 보기
| 구분 | Next.js 15 이하 | Next.js 16 이상 |
|---|---|---|
| 파일명 | middleware.ts |
proxy.ts |
| 함수명 | middleware() |
proxy() |
| 기능 | 동일 | 동일 |
| 위치 | src/middleware.ts 또는 루트 |
src/proxy.ts 또는 루트 |
| config 객체 | export const config |
export const config (동일) |
| 내보내기 방식 | 함수 export (default 또는 named) | 함수 export (default 또는 named) |
먼저, 기초부터 이해하기
proxy.ts는 언제 실행되나요?
Next.js의 프록시는 모든 요청이 서버에 도달하기 전에 실행됩니다.
[클라이언트]
↓
요청 (GET /about)
↓
[proxy.ts] ← 여기서 먼저 가로챔!
↓
리다이렉트? 인증 체크? 헤더 추가?
↓
[라우트 핸들러 또는 페이지]
↓
응답
↓
[클라이언트]
왜 필요한가요?
프록시가 없다면 어떤 일이 벌어질까요? 실제 시나리오를 떠올려보세요.
시나리오: 중간 규모 SaaS 애플리케이션
- 공개 페이지: 5개 (홈, 소개, 가격, 블로그, 연락처)
- 보호된 페이지: 20개 (대시보드, 설정, 프로필, 보고서 등)
- API 라우트: 15개
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
총 40개 파일
이제 다음과 같은 요구사항이 있다고 가정해봅시다:
- HTTPS 강제: 프로덕션에서 모든 HTTP 요청을 HTTPS로 리다이렉트
- 인증 체크: 보호된 페이지는 로그인 필수
- Rate Limiting: API는 분당 100회 제한
❌ 프록시 없이: 40개 파일에 반복 작성
// pages/about.tsx
export async function getServerSideProps({ req }) {
// 1. HTTPS 체크
if (process.env.NODE_ENV === 'production' &&
req.headers['x-forwarded-proto'] !== 'https') {
return { redirect: { destination: '...', permanent: false } };
}
// 실제 페이지 로직
return { props: {} };
}
// pages/dashboard.tsx
export async function getServerSideProps({ req }) {
// 1. HTTPS 체크 (복사-붙여넣기)
if (process.env.NODE_ENV === 'production' &&
req.headers['x-forwarded-proto'] !== 'https') {
return { redirect: { destination: '...', permanent: false } };
}
// 2. 인증 체크 (복사-붙여넣기)
if (!req.cookies.token) {
return { redirect: { destination: '/login', permanent: false } };
}
// 실제 페이지 로직
return { props: {} };
}
// pages/api/users.ts
export default async function handler(req, res) {
// 1. HTTPS 체크 (또 복사-붙여넣기)
if (process.env.NODE_ENV === 'production' &&
req.headers['x-forwarded-proto'] !== 'https') {
return res.status(403).json({ error: 'HTTPS required' });
}
// 2. 인증 체크 (또 복사-붙여넣기)
if (!req.cookies.token) {
return res.status(401).json({ error: 'Unauthorized' });
}
// 3. Rate Limiting (또 복사-붙여넣기)
// ...
// 실제 API 로직
}
// ... 37개 파일 더 ...
문제점:
- 코드 중복: 40개 파일에 같은 로직 반복
- 유지보수 지옥: HTTPS 체크 로직을 수정하려면 40개 파일을 다 수정해야 함
- 버그 가능성: 한 곳에서 실수하면 보안 취약점 발생
- 일관성 부족: 개발자마다 다르게 구현할 수 있음
- 성능 저하: 매번 같은 체크를 반복적으로 실행
실제로 제가 경험한 사례를 말씀드리자면, 한 팀원이 새로운 페이지를 추가하면서 인증 체크를 깜빡했습니다. 그 결과, 보호되어야 할 사용자 데이터가 며칠간 노출되었고, 사고 대응에 많은 시간을 소모했습니다.
✅ 프록시 사용: 1개 파일에만 작성
// proxy.ts (단 1개 파일)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. HTTPS 체크 (모든 요청에 적용)
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
const url = request.nextUrl.clone();
url.protocol = 'https:';
return NextResponse.redirect(url, 301);
}
// 2. 인증 체크 (보호된 경로만)
const protectedPaths = ['/dashboard', '/settings', '/profile'];
const isProtectedPath = protectedPaths.some(path => pathname.startsWith(path));
if (isProtectedPath && !request.cookies.get('token')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 3. Rate Limiting (API 경로만)
if (pathname.startsWith('/api/')) {
// Rate limiting 로직
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
이제 페이지들은 깔끔합니다:
// pages/about.tsx
export async function getServerSideProps() {
// HTTPS 체크? 이미 proxy.ts에서 처리됨!
// 바로 페이지 로직만 작성
return { props: {} };
}
// pages/dashboard.tsx
export async function getServerSideProps() {
// HTTPS 체크? proxy.ts에서 처리됨!
// 인증 체크? proxy.ts에서 처리됨!
// 바로 대시보드 데이터만 가져오면 됨
const data = await fetchDashboardData();
return { props: { data } };
}
// pages/api/users.ts
export default async function handler(req, res) {
// HTTPS, 인증, Rate Limiting 모두 proxy.ts에서 처리됨!
// 바로 비즈니스 로직만 작성
const users = await db.users.findMany();
res.json(users);
}
장점:
- 단일 진실 공급원: 보안 로직이 1곳에만 존재
- 쉬운 유지보수: 수정이 필요하면 proxy.ts 1개 파일만 수정
- 버그 감소: 한 곳에서 테스트하면 모든 곳에 적용됨
- 일관성 보장: 모든 요청이 동일한 검증을 거침
- 성능 향상: Edge Runtime에서 실행되어 빠름 (선택사항)
- 가독성: 각 페이지는 자신의 역할에만 집중
비교 요약:
| 항목 | 프록시 없이 | 프록시 사용 |
|---|---|---|
| 코드 중복 | 40개 파일에 반복 | 1개 파일에만 작성 |
| HTTPS 로직 수정 시 | 40개 파일 수정 | 1개 파일만 수정 |
| 새 페이지 추가 시 | 보안 로직 복사-붙여넣기 | 자동으로 적용됨 |
| 버그 발생 가능성 | 높음 (한 곳이라도 누락 시) | 낮음 (중앙 집중) |
| 코드 리뷰 | 매번 보안 로직 검토 필요 | 한 번만 검토하면 됨 |
| 테스트 | 40개 파일 각각 테스트 | 1개 파일만 테스트 |
이제 프록시가 왜 필요한지 명확하지 않나요? 단순히 편리함을 위한 기능이 아니라, 확장 가능하고 유지보수 가능한 애플리케이션을 만들기 위한 필수 도구입니다.
마이그레이션 가이드
단계 1: 파일명 변경
# src 디렉토리를 사용하는 경우
mv src/middleware.ts src/proxy.ts
# 루트 디렉토리에 있는 경우
mv middleware.ts proxy.ts
주의: Git을 사용한다면, git mv 명령어를 사용하세요. 파일 이력이 유지됩니다.
git mv src/middleware.ts src/proxy.ts
단계 2: 함수명 변경
Before (middleware.ts)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// HTTPS 강제 (프로덕션 환경에서만)
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
const url = request.nextUrl.clone();
url.protocol = 'https:';
return NextResponse.redirect(url, 301);
}
return NextResponse.next();
}
export const config = {
matcher: '/:path*',
};
After (proxy.ts)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
// HTTPS 강제 (프로덕션 환경에서만)
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
const url = request.nextUrl.clone();
url.protocol = 'https:';
return NextResponse.redirect(url, 301);
}
return NextResponse.next();
}
export const config = {
matcher: '/:path*',
};
변경된 부분: middleware → proxy (1개 단어만 변경)
단계 3: 테스트
마이그레이션 후, 다음을 확인하세요.
# 개발 서버 시작
npm run dev
# 또는
yarn dev
✅ 성공 시 출력:
✓ Starting...
✓ Ready in 390ms
GET /privacy 200 in 1699ms (compile: 1543ms, proxy.ts: 53ms, render: 103ms)
출력에 proxy.ts: 53ms가 나타나면 성공입니다!
❌ 실패 시 출력:
⨯ The file "./proxy.ts" must export a function,
either as a default export or as a named "proxy" export.
이 에러가 나타나면, 함수명이 여전히 middleware로 되어 있는지 확인하세요.
실전 예제
예제 1: HTTPS 강제 리다이렉트
시나리오: 프로덕션 환경에서 모든 HTTP 요청을 HTTPS로 리다이렉트
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
// 프로덕션 환경에서만 HTTPS 체크
if (process.env.NODE_ENV === 'production') {
const proto = request.headers.get('x-forwarded-proto');
if (proto !== 'https') {
const url = request.nextUrl.clone();
url.protocol = 'https:';
return NextResponse.redirect(url, 301); // 영구 리다이렉트
}
}
return NextResponse.next();
}
export const config = {
matcher: '/:path*', // 모든 경로에 적용
};
동작 방식:
클라이언트 → http://example.com/about
↓
proxy.ts에서 감지
↓
301 리다이렉트 → https://example.com/about
↓
브라우저가 HTTPS로 재요청
예제 2: 인증 체크
시나리오: 로그인하지 않은 사용자를 /login으로 리다이렉트
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const token = request.cookies.get('auth-token');
const { pathname } = request.nextUrl;
// 보호된 경로 목록
const protectedPaths = ['/dashboard', '/profile', '/settings'];
// 현재 경로가 보호된 경로인지 확인
const isProtectedPath = protectedPaths.some(path =>
pathname.startsWith(path)
);
// 토큰이 없고, 보호된 경로라면 로그인 페이지로
if (!token && isProtectedPath) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname); // 리다이렉트 후 돌아갈 경로
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
동작 방식:
/dashboard 접근 시도
↓
proxy.ts: 쿠키 확인
↓
auth-token 없음 → /login?from=/dashboard로 리다이렉트
↓
로그인 성공 후 → /dashboard로 복귀
예제 3: 지역화 (i18n)
시나리오: 사용자의 언어에 따라 자동으로 경로 prefix 추가
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// 이미 언어 prefix가 있는지 확인
const locales = ['en', 'ko', 'ja'];
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) {
return NextResponse.next();
}
// Accept-Language 헤더에서 선호 언어 추출
const acceptLanguage = request.headers.get('accept-language') || '';
const preferredLocale = acceptLanguage.split(',')[0].split('-')[0];
// 지원하는 언어인지 확인
const locale = locales.includes(preferredLocale) ? preferredLocale : 'en';
// 언어 prefix 추가하여 리다이렉트
const url = request.nextUrl.clone();
url.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(url);
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
동작 방식:
/about 접근 (브라우저 언어: 한국어)
↓
proxy.ts: Accept-Language 헤더 확인
↓
리다이렉트 → /ko/about
예제 4: A/B 테스팅
시나리오: 사용자를 무작위로 두 버전의 페이지로 분배
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// 홈페이지만 A/B 테스팅
if (pathname === '/') {
// 쿠키에 이미 버전이 저장되어 있는지 확인
const abTestCookie = request.cookies.get('ab-test-version');
if (!abTestCookie) {
// 50% 확률로 버전 결정
const version = Math.random() < 0.5 ? 'a' : 'b';
const response = NextResponse.next();
response.cookies.set('ab-test-version', version, {
maxAge: 60 * 60 * 24 * 30, // 30일
});
// 헤더에 버전 정보 추가 (페이지에서 사용 가능)
response.headers.set('x-ab-test-version', version);
return response;
}
}
return NextResponse.next();
}
export const config = {
matcher: '/',
};
페이지에서 버전 확인:
// app/page.tsx
export default function Home() {
return (
<>
{/* 버전 A와 B를 동시에 렌더링하고, CSS로 숨김 처리 */}
</>
);
}
// 또는 서버 컴포넌트에서
export default async function Home() {
const headers = await headers();
const version = headers.get('x-ab-test-version');
if (version === 'b') {
return <HomepageB />;
}
return <HomepageA />;
}
함정과 주의사항
프록시를 잘못 사용하면 성능 문제나 무한 루프가 발생할 수 있습니다.
함정 1: 무한 리다이렉트 루프
// ❌ 나쁜 예: 무한 루프 발생
export function proxy(request: NextRequest) {
const url = request.nextUrl.clone();
url.pathname = '/redirected';
return NextResponse.redirect(url); // 매번 리다이렉트!
}
문제: /redirected 경로도 프록시를 거치므로, 다시 리다이렉트가 발생합니다.
// ✅ 좋은 예: 조건 추가
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// 이미 리다이렉트된 경로는 제외
if (pathname !== '/redirected' && someCondition) {
const url = request.nextUrl.clone();
url.pathname = '/redirected';
return NextResponse.redirect(url);
}
return NextResponse.next();
}
함정 2: 모든 경로에 프록시 적용
// ❌ 나쁜 예: 정적 파일까지 프록시 처리
export const config = {
matcher: '/:path*', // 모든 경로!
};
문제: /_next/static/*, /favicon.ico 같은 정적 파일까지 프록시를 거쳐서 성능 저하가 발생합니다.
// ✅ 좋은 예: 정적 파일 제외
export const config = {
matcher: [
/*
* 다음을 제외한 모든 경로:
* - api (API routes)
* - _next/static (정적 파일)
* - _next/image (이미지 최적화)
* - favicon.ico (파비콘)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
함정 3: 프록시에서 데이터베이스 쿼리
// ❌ 나쁜 예: 매 요청마다 DB 쿼리
export async function proxy(request: NextRequest) {
const user = await db.users.findUnique({
where: { id: request.cookies.get('user-id') }
});
if (!user) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
문제: 프록시는 모든 요청에서 실행됩니다. DB 쿼리가 병목이 될 수 있습니다.
// ✅ 좋은 예: JWT 검증 (DB 쿼리 없음)
import { jwtVerify } from 'jose';
export async function proxy(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
// JWT 토큰 검증 (DB 쿼리 없이 서명만 확인)
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET!)
);
return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
더 나은 방법: Edge Runtime 사용
// Edge Runtime은 전 세계 CDN에서 실행되어 지연 시간이 낮습니다
export const runtime = 'edge';
export function proxy(request: NextRequest) {
// ...
}
함정 4: 함수명을 바꾸지 않음
// ❌ 나쁜 예: 파일명만 바꿈
// proxy.ts
export function middleware(request: NextRequest) {
// ...
}
에러:
⨯ The file "./proxy.ts" must export a function,
either as a default export or as a named "proxy" export.
// ✅ 좋은 예: 함수명도 변경
// proxy.ts
export function proxy(request: NextRequest) {
// ...
}
// 또는 default export
export default function proxy(request: NextRequest) {
// ...
}
마이그레이션 체크리스트
마이그레이션을 완료하기 전에 다음을 확인하세요.
- 파일명을
middleware.ts에서proxy.ts로 변경했나요? - 함수명을
middleware에서proxy로 변경했나요? - 개발 서버를 재시작했나요? (
npm run dev) - Deprecation 경고가 사라졌나요?
- 터미널에
proxy.ts: XXms가 표시되나요? - 기존 기능이 모두 정상 작동하나요?
- HTTPS 리다이렉트
- 인증 체크
- 기타 커스텀 로직
config.matcher가 정적 파일을 제외하고 있나요?- Git 커밋 메시지가 명확한가요? (예:
chore: migrate middleware to proxy (Next.js 16))
테스트 결과 확인하기
마이그레이션 후, 다음과 같은 출력을 확인하세요.
✅ 성공한 경우
$ npm run dev
✓ Starting...
✓ Ready in 390ms
Local: http://localhost:3000
Network: http://192.168.1.100:3000
GET /privacy 200 in 1699ms (compile: 1543ms, proxy.ts: 53ms, render: 103ms)
GET /about 200 in 245ms (proxy.ts: 12ms, render: 233ms)
확인 포인트:
- ✅ Deprecation 경고가 없음
- ✅
proxy.ts: XXms표시 (프록시가 실행되고 있음) - ✅ 페이지가 정상적으로 렌더링됨
❌ 실패한 경우
케이스 1: 함수명을 바꾸지 않음
⨯ The file "./proxy.ts" must export a function,
either as a default export or as a named "proxy" export.
해결책: 함수명을 proxy로 변경하세요.
케이스 2: 파일이 두 개 존재
⚠ Multiple proxy files detected:
- src/middleware.ts
- src/proxy.ts
Only one will be used. Please remove the deprecated file.
해결책: middleware.ts 파일을 삭제하세요.
rm src/middleware.ts
# 또는
git rm src/middleware.ts
실전 활용 팁
팁 1: 타입 안정성 확보
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// 프록시 함수 타입 정의
type ProxyFunction = (request: NextRequest) => Promise<NextResponse> | NextResponse;
export const proxy: ProxyFunction = (request) => {
// TypeScript가 반환 타입을 체크합니다
return NextResponse.next();
};
팁 2: 환경별 로직 분리
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
export function proxy(request: NextRequest) {
// 개발 환경에서만 실행
if (isDevelopment) {
console.log(`[DEV] ${request.method} ${request.nextUrl.pathname}`);
}
// 프로덕션 환경에서만 HTTPS 강제
if (isProduction && request.headers.get('x-forwarded-proto') !== 'https') {
const url = request.nextUrl.clone();
url.protocol = 'https:';
return NextResponse.redirect(url, 301);
}
return NextResponse.next();
}
팁 3: 여러 프록시 로직 합성
// lib/proxy/https.ts
export function enforceHttps(request: NextRequest) {
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
const url = request.nextUrl.clone();
url.protocol = 'https:';
return NextResponse.redirect(url, 301);
}
return null;
}
// lib/proxy/auth.ts
export function checkAuth(request: NextRequest) {
const token = request.cookies.get('auth-token');
const { pathname } = request.nextUrl;
if (!token && pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return null;
}
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { enforceHttps } from './lib/proxy/https';
import { checkAuth } from './lib/proxy/auth';
export function proxy(request: NextRequest) {
// 각 프록시 로직을 순차적으로 실행
const httpsResponse = enforceHttps(request);
if (httpsResponse) return httpsResponse;
const authResponse = checkAuth(request);
if (authResponse) return authResponse;
// 모든 체크를 통과하면 다음으로
return NextResponse.next();
}
이렇게 하면 각 로직을 독립적으로 테스트할 수 있고, 재사용성도 높아집니다.
왜 지금 마이그레이션해야 할까요?
1. Deprecation은 경고가 아니라 예고입니다
Next.js 16에서 middleware.ts는 아직 작동하지만, deprecation 경고가 표시됩니다. 이는 앞으로 완전히 제거될 것이라는 신호입니다.
보통 Next.js는 다음과 같은 일정을 따릅니다:
버전 16: Deprecation 경고 (현재)
↓
버전 17: 경고 강화 + 마이그레이션 가이드
↓
버전 18: 완전 제거 (에러 발생)
지금 마이그레이션하면, 나중에 급하게 수정할 필요가 없습니다.
2. 새로운 기능은 proxy.ts에만 추가됩니다
Next.js 팀은 앞으로 새로운 기능을 proxy.ts에만 추가할 것입니다. middleware.ts를 계속 사용하면, 최신 기능을 사용할 수 없게 됩니다.
3. 마이그레이션이 간단합니다
대부분의 프로젝트에서 5분 이내에 완료할 수 있습니다.
# 1. 파일 이름 변경 (10초)
git mv src/middleware.ts src/proxy.ts
# 2. 함수명 변경 (30초)
# middleware → proxy
# 3. 테스트 (1분)
npm run dev
# 4. 커밋 (1분)
git commit -m "chore: migrate middleware to proxy (Next.js 16)"
더 알아보기
공식 문서
관련 자료
다음 단계
이제 proxy.ts로 마이그레이션했으니, 다음을 시도해보세요:
- A/B 테스팅 구현: 사용자를 무작위로 두 버전으로 분배
- Rate Limiting: IP별 요청 제한 (Edge Runtime + KV 스토리지)
- Bot Detection: User-Agent 기반 봇 필터링
- Feature Flags: 특정 사용자에게만 새 기능 활성화
마무리
Next.js 16의 proxy.ts 전환은 단순한 이름 변경이 아니라, 더 명확하고 직관적인 API로의 발전입니다.
핵심 정리:
- ✅ 파일명:
middleware.ts→proxy.ts - ✅ 함수명:
middleware()→proxy() - ✅ 기능은 동일하지만, 개념이 더 명확해짐
- ✅ 5분 이내에 마이그레이션 가능
- ✅ 앞으로 새로운 기능은
proxy.ts에만 추가됨
마이그레이션에 어려움이 있다면, GitHub Discussions에서 커뮤니티의 도움을 받을 수 있습니다.
댓글