Next.js Rewrites
프론트엔드에서 외부 API를 호출할 때 가장 먼저 부딪히는 문제가 CORS다. 브라우저는 현재 도메인과 다른 도메인으로 요청을 보낼 때 CORS 정책을 적용하는데, API 서버에서 적절한 헤더를 설정하지 않으면 요청이 차단된다.
Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000'
has been blocked by CORS policy
이 문제를 해결하는 전통적인 방법은 세 가지다.
- API 서버에서 CORS 헤더 추가 — 백엔드 팀이
Access-Control-Allow-Origin을 설정한다. 근데 개발 환경에서만 필요한 설정을 프로덕션 서버에 넣는 건 찝찝하다. - 프록시 서버 구축 — Nginx나 별도 서버를 두고 요청을 중계한다. 설정이 번거롭고, 개발 환경마다 따로 관리해야 한다.
- 브라우저 CORS 확장 프로그램 — 개발용으로 쓸 수는 있지만 팀 전체가 이걸 쓰라고 할 수는 없다.
Next.js Rewrites는 이 문제를 프레임워크 레벨에서 해결한다. 클라이언트가 같은 도메인(Next.js 서버)으로 요청을 보내면, Next.js가 서버 사이드에서 실제 API 서버로 요청을 전달하고 응답을 돌려준다. 브라우저 입장에서는 같은 도메인 요청이니까 CORS 문제가 발생하지 않는다.
Rewrites vs Redirects
이름이 비슷한 Redirects와 혼동하기 쉬운데, 동작 방식이 완전히 다르다.
Redirects는 브라우저에게 "이 URL 말고 저쪽으로 가"라고 알려준다. 브라우저 주소창의 URL이 바뀌고, 클라이언트가 새 URL로 다시 요청을 보낸다. HTTP 301이나 302 응답이다.
클라이언트 → /old-page → 302 → /new-page (URL 변경됨)
Rewrites는 URL을 바꾸지 않는다. 클라이언트는 원래 URL로 요청했다고 생각하지만, 서버가 내부적으로 다른 곳에서 응답을 가져와서 돌려준다. URL 마스킹이다.
클라이언트 → /api/proxy/users → (서버 내부) → https://api.example.com/users
(클라이언트는 모름)
이 차이가 중요한 이유는, Rewrites가 프록시처럼 동작하기 때문이다. 클라이언트는 자기 도메인으로 요청을 보냈다고 생각하니까 CORS 문제가 없고, 실제 API 서버 주소가 클라이언트에 노출되지 않는다.
기본 설정
next.config.ts(또는 .js, .mjs)에서 rewrites 함수를 정의한다.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/api/proxy/:path*',
destination: 'https://api.example.com/:path*',
},
];
},
};
export default nextConfig;
이 설정 하나로 /api/proxy/users 요청이 https://api.example.com/users로 전달된다. :path*는 와일드카드 매칭으로, 슬래시를 포함한 모든 경로 세그먼트를 캡처한다.
클라이언트 코드에서는 이렇게 사용한다:
// 이전: CORS 에러 발생
const res = await fetch('https://api.example.com/users');
// 이후: 같은 도메인이라 CORS 없음
const res = await fetch('/api/proxy/users');
source와 destination 패턴
고정 경로
가장 단순한 형태. 특정 경로 하나를 다른 곳으로 매핑한다.
{
source: '/about',
destination: '/about-us',
}
파라미터 매칭 (:param)
경로의 일부를 변수로 캡처해서 destination에서 사용할 수 있다.
{
source: '/blog/:slug',
destination: '/posts/:slug',
}
// /blog/hello-world → /posts/hello-world
와일드카드 매칭 (:path*)
슬래시를 포함한 여러 세그먼트를 한 번에 캡처한다. API 프록시에서 가장 많이 쓰는 패턴이다.
{
source: '/api/proxy/:path*',
destination: 'https://api.example.com/:path*',
}
// /api/proxy/users/123/posts → https://api.example.com/users/123/posts
* 없이 :path만 쓰면 단일 세그먼트만 매칭된다. /api/proxy/users는 매칭되지만 /api/proxy/users/123은 매칭되지 않는다.
정규식 매칭
더 세밀한 제어가 필요하면 정규식을 쓸 수 있다. 파라미터 뒤에 괄호로 정규식을 지정한다.
{
source: '/api/:version(v[0-9]+)/:path*',
destination: 'https://api.example.com/:version/:path*',
}
// /api/v2/users → https://api.example.com/v2/users
// /api/latest/users → 매칭 안 됨
쿼리스트링 처리
쿼리스트링은 자동으로 destination에 전달된다. 별도 설정이 필요 없다.
{
source: '/api/proxy/:path*',
destination: 'https://api.example.com/:path*',
}
// /api/proxy/users?page=2&limit=10
// → https://api.example.com/users?page=2&limit=10
단, destination에 이미 쿼리스트링이 포함되어 있으면 source의 쿼리스트링은 무시된다.
{
source: '/api/proxy/:path*',
destination: 'https://api.example.com/:path*?apiKey=secret',
}
// /api/proxy/users?page=2
// → https://api.example.com/users?apiKey=secret (page=2는 무시됨)
이 동작을 이용해서 API 키를 서버 사이드에서 주입할 수 있다. 클라이언트에 API 키를 노출하지 않아도 된다.
실행 순서와 단계
Rewrites는 세 단계로 나눠서 적용 시점을 제어할 수 있다.
async rewrites() {
return {
beforeFiles: [
// 1단계: 페이지/정적 파일 매칭 전에 실행
],
afterFiles: [
// 2단계: 페이지는 매칭됐지만 동적 라우트 전에 실행
],
fallback: [
// 3단계: 페이지/동적 라우트 매칭 다 끝난 후 실행
],
};
}
배열만 반환하면 기본적으로 afterFiles 단계에서 실행된다. 대부분의 API 프록시 용도에서는 이 기본값이면 충분하다.
beforeFiles가 유용한 경우: 같은 경로에 실제 페이지도 있고 rewrite 규칙도 있을 때. 예를 들어, A/B 테스트에서 특정 조건일 때 다른 페이지를 보여주고 싶을 때 사용한다.
beforeFiles: [
{
source: '/landing',
has: [{ type: 'cookie', key: 'experiment', value: 'new' }],
destination: '/landing-v2',
},
],
fallback은 404 페이지 대신 다른 서비스로 요청을 전달할 때 유용하다. 마이크로 프론트엔드에서 현재 앱에 없는 라우트를 다른 앱으로 프록시하는 패턴이다.
has/missing 조건
특정 조건에서만 rewrite를 적용할 수 있다. 헤더, 쿠키, 쿼리 파라미터의 존재 여부나 값을 기준으로 분기한다.
{
source: '/api/:path*',
has: [
{ type: 'header', key: 'x-custom-header' },
],
destination: 'https://api-v2.example.com/:path*',
}
이 규칙은 x-custom-header 헤더가 있는 요청에만 적용된다. missing은 반대로 특정 조건이 없을 때 적용된다.
{
source: '/:path*',
missing: [
{ type: 'header', key: 'x-skip-rewrite' },
],
destination: '/proxy/:path*',
}
조건 타입은 header, cookie, query, host 네 가지가 있고, value에 정규식을 사용할 수도 있다.
has: [
{ type: 'query', key: 'lang', value: '(?<lang>ko|ja|zh)' },
],
destination: '/i18n/:lang/:path*',
named capture group (?<lang>...)으로 캡처한 값을 destination에서 사용할 수 있다.
환경별 분기
개발 환경과 프로덕션 환경에서 다른 API 서버를 사용하는 경우가 많다. 환경 변수로 분기하면 된다.
async rewrites() {
const apiUrl = process.env.API_URL || 'https://dev.api.example.com';
return [
{
source: '/api/proxy/:path*',
destination: `${apiUrl}/:path*`,
},
];
}
.env.local과 .env.production에 각각 다른 API_URL을 설정해두면 환경별로 자동 분기된다. 이 방식의 장점은 클라이언트 코드를 한 줄도 수정하지 않아도 된다는 것이다.
실제 프로젝트 적용 예시
CORS 문제를 해결하기 위해 API 프록시를 설정하는 전형적인 패턴이다.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/api/proxy/:path*',
destination: 'https://dev.api.youvico.com/:path*',
},
];
},
};
클라이언트에서 모든 API 호출을 /api/proxy/ 접두사로 보내면, Next.js 서버가 실제 API 서버로 요청을 전달한다. 프로덕션에서는 Nginx 같은 리버스 프록시가 이 역할을 대신하므로, 주로 개발 환경에서 유용하게 사용된다.
이때 주의할 점이 있다. Next.js의 API Route(/api/ 경로)와 rewrite 경로가 겹치면 API Route가 우선 적용된다. 그래서 /api/proxy/처럼 충돌하지 않는 접두사를 사용하는 게 좋다.
Rewrites vs Route Handler vs Middleware
비슷한 역할을 할 수 있는 세 가지 기능을 비교해보자.
Rewrites는 설정 기반이다. 코드를 작성하지 않고 next.config에서 매핑 규칙만 정의한다. 요청/응답을 변환할 수 없고, 단순히 경로를 다른 곳으로 전달하기만 한다. API 프록시나 URL 마스킹에 적합하다.
Route Handler(app/api/route.ts)는 코드 기반이다. 요청을 받아서 가공하고, 외부 API를 호출하고, 응답을 변환해서 돌려줄 수 있다. 토큰 교환, 요청 검증, 응답 변환이 필요한 경우에 사용한다.
// app/api/proxy/[...path]/route.ts
export async function GET(request: Request) {
const url = new URL(request.url);
const path = url.pathname.replace('/api/proxy', '');
// 헤더 추가, 인증 토큰 주입 등 커스텀 로직
const res = await fetch(`https://api.example.com${path}`, {
headers: { Authorization: `Bearer ${getToken()}` },
});
return Response.json(await res.json());
}
Middleware는 모든 요청에 대해 실행되는 Edge 함수다. Rewrite를 동적으로 수행할 수 있지만, Edge Runtime 제약이 있어서 Node.js API를 사용할 수 없다. 인증 체크 후 리다이렉트나 A/B 테스트 분기에 주로 사용한다.
| 기능 | 설정 방식 | 요청 가공 | 응답 가공 | 적합한 용도 |
|---|---|---|---|---|
| Rewrites | next.config | ❌ | ❌ | 단순 프록시, URL 마스킹 |
| Route Handler | 코드 | ✅ | ✅ | API 프록시 + 로직 |
| Middleware | 코드 (Edge) | 제한적 | 제한적 | 인증, A/B 테스트 |
단순히 CORS 해결만 필요하다면 Rewrites가 가장 간단하다. 요청에 인증 토큰을 넣거나 응답을 변환해야 하면 Route Handler를 쓴다.
주의사항
프로덕션 배포 시
Rewrites는 Next.js 서버에서 처리되므로, Vercel이나 자체 서버에서는 그대로 동작한다. 하지만 정적 빌드(next export)에서는 Rewrites가 동작하지 않는다. 서버가 없으면 프록시를 할 주체가 없기 때문이다.
성능
모든 API 요청이 Next.js 서버를 한 번 거쳐가므로, 직접 API 서버에 요청하는 것보다 레이턴시가 추가된다. 개발 환경에서는 문제 없지만, 프로덕션에서 대량의 API 호출을 Rewrites로 처리하는 건 권장하지 않는다. 프로덕션에서는 Nginx나 CDN 레벨에서 프록시를 설정하는 게 좋다.
헤더 전달
Rewrites는 클라이언트의 요청 헤더를 destination으로 전달한다. 하지만 Host 헤더는 destination의 호스트로 변경된다. 일부 API 서버가 Host 헤더를 검증하는 경우 문제가 될 수 있다.