Next.js App Router: Route Handler CORS 에러 해결 및 OPTIONS 프리플라이트 응답 헤더 설정가이드
- 원인: Route Handler가
Access-Control-Allow-Origin헤더 없이 응답하면, 브라우저가 다른 출처에서 온 요청이라 판단해 응답을 강제로 막음 - 해결: 라우터 개별 응답에 CORS 헤더 직접 세팅 + OPTIONS 프리플라이트 대응 / 엔드포인트가 많으면
middleware.ts로 일괄 적용 - 주의: 프론트엔드와 백엔드를 서로 다른 도메인·포트로 분리 배포했을 때 이 에러를 가장 흔하게 마주침
headers: { 'Access-Control-Allow-Origin': 'https://my-app-client.com' }
↓ 왜 이런 에러가 나는지 원리 보기
Next.js Route Handler로 외부 도메인에 API를 열어주려 하면 브라우저 콘솔에 CORS 에러가 뜨는 경우가 많습니다. 로컬에서는 문제없이 통신되던 요청이 배포 도메인에서는 막히는 이유는 브라우저의 동일 출처 정책 때문입니다. 원인과 대응 코드 두 가지를 정리합니다.
Route Handler에서 CORS 에러가 발생하는 원인
CORS(Cross-Origin Resource Sharing: 교차 출처 리소스 공유)는 브라우저가 사용자 보안을 지키기 위해 구현한 동일 출처 정책(SOP)에서 비롯됩니다. 기본적으로 Next.js Route Handler(app/api/route.ts)는 외부 클라이언트가 요청을 보낼 때, 응답 헤더에 "이 외부 도메인의 요청을 허가한다"는 승인 표식을 남겨서 보내주어야 합니다. 이 표식이 바로 Access-Control-Allow-Origin 헤더입니다. Next.js 서버에서 이 헤더를 명시해 주지 않은 채 데이터를 응답하면, 브라우저는 해킹의 위험이 있다고 간주하여 데이터를 강제로 누락시키고 CORS 에러를 발생시킵니다. 프론트엔드와 백엔드를 서로 다른 도메인이나 포트로 분리 배포했을 때 이 에러를 가장 흔하게 마주치게 됩니다.
CORS 보안 필터링 통과 규칙
Route Handler에서 CORS 문제를 해결하는 2가지 솔루션
방법 A: Route Handler 개별 응답 헤더 세팅
특정 API 라우터(app/api/data/route.ts) 내부에서 들어오는 요청에 대해 승인 헤더를 수동으로 적용하여 응답하는 방법입니다.
import { NextResponse } from 'next/server';
export async function GET() {
const data = { message: "안전한 데이터입니다." };
return NextResponse.json(data, {
headers: {
// 1. 특정 외부 클라이언트 주소 허용 (모든 도메인 허용 시 '*' 입력 가능)
'Access-Control-Allow-Origin': 'https://my-app-client.com',
// 2. 허용할 HTTP 메소드 나열
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
// 3. 브라우저가 커스텀 헤더를 전송할 수 있게 승인
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
// 브라우저가 본 요청을 보내기 전 미리 안전한지 검사하는 OPTIONS 요청(Preflight)에 대응
export async function OPTIONS() {
return new NextResponse(null, {
status: 204, // No Content
headers: {
'Access-Control-Allow-Origin': 'https://my-app-client.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
외부 파트너사에 API를 열어주는 작업에서 GET 응답에만 CORS 헤더를 넣고 OPTIONS 핸들러를 깜빡한 적이 있습니다. 브라우저 콘솔에는 여전히 CORS 에러가 떠서 헤더 설정이 잘못된 줄 알고 한참 값을 바꿔봤는데, 알고 보니 프리플라이트 요청 자체가 별도 핸들러 없이는 204조차 못 받고 막히고 있었습니다.
방법 B: Next.js 미들웨어(middleware.ts)로 글로벌 CORS 세팅
API 엔드포인트가 수십 개가 넘어 매번 소스코드 헤더를 수동 지정하기 어렵다면, 모든 API 통로의 최전방에 위치하는 미들웨어 파일 하나로 일괄 처리하는 것이 좋습니다.
[루트 경로 생성: middleware.ts]
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 모든 API (/api/:path*) 요청에 대해 CORS 응답 헤더 일괄 주입
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return response;
}
// 미들웨어가 감시할 경로 매칭 패턴 설정
export const config = {
matcher: '/api/:path*',
};
요약: CORS 정책 대응 핵심 요약 매트릭스
특정 도메인에만 폐쇄적으로 데이터를 제공하고 싶다면 Route Handler마다 개별적으로 headers와 OPTIONS를 정의하는 방식이 보안 측면에서 가장 우수합니다. API 엔드포인트가 많아 중복 코드가 부담스럽다면 middleware.ts에서 CORS 헤더를 일괄 적용하는 편이 관리 효율이 좋습니다.
웹 표준 CORS 통신 규약과 브라우저 Preflight 정책에 대한 상세 내용은 아래 문서에서 확인할 수 있습니다.
참고로 Access-Control-Allow-Origin을 '*'로 열어두면 어떤 도메인에서도 요청을 받을 수 있지만, 인증이 필요한 API라면 허용 도메인을 명시적으로 제한하는 편이 안전합니다.
댓글
댓글 쓰기