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.tsproxy.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는 더 명확한 이름입니다. 이 기능이 실제로 하는 일을 생각해보면:

  1. HTTP 요청을 HTTPS로 바꿔서 전달 (클라이언트는 모름)
  2. 로그인이 안 된 사용자를 로그인 페이지로 보내기 (원래 페이지는 실행 안 됨)
  3. 사용자 언어에 따라 /ko/about 또는 /en/about으로 분기
  4. 특정 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개 파일

이제 다음과 같은 요구사항이 있다고 가정해봅시다:

  1. HTTPS 강제: 프로덕션에서 모든 HTTP 요청을 HTTPS로 리다이렉트
  2. 인증 체크: 보호된 페이지는 로그인 필수
  3. 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개 파일 더 ...

문제점:

  1. 코드 중복: 40개 파일에 같은 로직 반복
  2. 유지보수 지옥: HTTPS 체크 로직을 수정하려면 40개 파일을 다 수정해야 함
  3. 버그 가능성: 한 곳에서 실수하면 보안 취약점 발생
  4. 일관성 부족: 개발자마다 다르게 구현할 수 있음
  5. 성능 저하: 매번 같은 체크를 반복적으로 실행

실제로 제가 경험한 사례를 말씀드리자면, 한 팀원이 새로운 페이지를 추가하면서 인증 체크를 깜빡했습니다. 그 결과, 보호되어야 할 사용자 데이터가 며칠간 노출되었고, 사고 대응에 많은 시간을 소모했습니다.

✅ 프록시 사용: 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. 단일 진실 공급원: 보안 로직이 1곳에만 존재
  2. 쉬운 유지보수: 수정이 필요하면 proxy.ts 1개 파일만 수정
  3. 버그 감소: 한 곳에서 테스트하면 모든 곳에 적용됨
  4. 일관성 보장: 모든 요청이 동일한 검증을 거침
  5. 성능 향상: Edge Runtime에서 실행되어 빠름 (선택사항)
  6. 가독성: 각 페이지는 자신의 역할에만 집중

비교 요약:

항목 프록시 없이 프록시 사용
코드 중복 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*',
};

변경된 부분: middlewareproxy (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로 마이그레이션했으니, 다음을 시도해보세요:

  1. A/B 테스팅 구현: 사용자를 무작위로 두 버전으로 분배
  2. Rate Limiting: IP별 요청 제한 (Edge Runtime + KV 스토리지)
  3. Bot Detection: User-Agent 기반 봇 필터링
  4. Feature Flags: 특정 사용자에게만 새 기능 활성화

마무리

Next.js 16의 proxy.ts 전환은 단순한 이름 변경이 아니라, 더 명확하고 직관적인 API로의 발전입니다.

핵심 정리:

  • ✅ 파일명: middleware.tsproxy.ts
  • ✅ 함수명: middleware()proxy()
  • ✅ 기능은 동일하지만, 개념이 더 명확해짐
  • ✅ 5분 이내에 마이그레이션 가능
  • ✅ 앞으로 새로운 기능은 proxy.ts에만 추가됨

마이그레이션에 어려움이 있다면, GitHub Discussions에서 커뮤니티의 도움을 받을 수 있습니다.

댓글