Same-Origin Policy부터 CORS 헤더, Preflight 요청, Express·Nginx 설정까지 CORS 차단 원인과 해결법을 실전 코드와 함께 정리한다. 개발·운영 환경별 설정 가이드를 포함한다.
한 줄 요약: CORS는 브라우저가 다른 출처의 리소스 요청을 제한하는 보안 정책이다. 서버가 올바른 헤더를 응답에 포함해야만 브라우저가 응답을 허용한다.
개발하다 보면 반드시 한 번은 마주치는 에러가 있다. Access to XMLHttpRequest at 'http://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy. 이 에러가 뜨는 이유가 뭔지, 어디서 무엇을 고쳐야 하는지 정확히 이해하면 다음번엔 30초 안에 해결할 수 있다.
Same-Origin Policy — CORS가 존재하는 이유
브라우저는 기본적으로 Same-Origin Policy(동일 출처 정책)를 적용한다. 'Origin'은 프로토콜 + 호스트 + 포트의 조합이다. 이 세 가지 중 하나라도 다르면 '다른 출처'로 판단한다.
비교 URL
기준 URL: http://example.com
동일 출처?
http://example.com/api
경로만 다름
동일
https://example.com
프로토콜 다름
다름
http://api.example.com
서브도메인 다름
다름
http://example.com:8080
포트 다름
다름
http://other.com
호스트 다름
다름
Same-Origin Policy가 없으면 악성 사이트가 사용자 브라우저를 통해 로그인된 다른 사이트에 요청을 보내고 응답을 읽을 수 있다. CORS는 이 정책의 예외를 서버가 명시적으로 허용하는 메커니즘이다.
핵심 포인트: CORS는 서버가 아니라 브라우저가 실행하는 정책이다. curl이나 Postman에서는 CORS 에러가 없다. 브라우저에서만 발생한다.
CORS 완벽 이해 — 왜 차단되고 어떻게 해결하나 — 보안 아키텍처 다이어그램 (출처: 공식 문서 및 벤치마크 데이터 기반)
CORS 헤더 — 서버가 응답에 무엇을 넣어야 하나
서버가 교차 출처 요청을 허용하려면 응답 헤더에 CORS 관련 헤더를 포함해야 한다. 주요 헤더와 역할은 다음과 같다.
헤더
설명
예시 값
Access-Control-Allow-Origin
허용할 출처
https://example.com 또는 *
Access-Control-Allow-Methods
허용할 HTTP 메서드
GET, POST, PUT, DELETE
Access-Control-Allow-Headers
허용할 요청 헤더
Content-Type, Authorization
Access-Control-Allow-Credentials
쿠키/인증 포함 허용
true
Access-Control-Max-Age
Preflight 결과 캐시 시간(초)
86400
Access-Control-Expose-Headers
JS에서 접근 허용할 응답 헤더
X-Total-Count
CORS 완벽 이해 — 왜 차단되고 어떻게 해결하나 — 위협 모델 시각화 (출처: 공식 문서 및 벤치마크 데이터 기반)
Preflight 요청 — OPTIONS 메서드의 정체
단순 요청(GET, POST, HEAD + 기본 헤더만 사용)이 아닌 경우, 브라우저는 실제 요청 전에 OPTIONS 메서드로 Preflight(사전 확인) 요청을 먼저 보낸다. 서버가 이 요청에 올바른 CORS 헤더로 응답해야 실제 요청이 전송된다.
Preflight가 발생하는 조건
PUT, DELETE, PATCH 메서드 사용
Content-Type: application/json 같은 비표준 Content-Type 사용
Authorization 헤더 포함
커스텀 헤더 사용 (예: X-Custom-Header)
Preflight 요청/응답 흐름 예시
// 브라우저가 먼저 보내는 Preflight 요청
OPTIONS /api/data HTTP/1.1
Origin: https://frontend.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
// 서버가 응답해야 하는 내용
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
// Preflight 통과 후 실제 요청이 전송됨
POST /api/data HTTP/1.1
Origin: https://frontend.example.com
Content-Type: application/json
Authorization: Bearer eyJ...
실전 설정 — Express와 Nginx에서 CORS 처리
CORS 설정은 두 레벨에서 가능하다. 애플리케이션 레벨(Node.js/Express)이 더 유연하고, Nginx 레벨은 여러 백엔드를 동시에 처리할 때 유리하다. 둘 다 설정하면 헤더가 중복될 수 있으니 한 레벨에서만 처리하는 것을 권장한다.
CORS 완벽 이해 — 왜 차단되고 어떻게 해결하나 — 취약점 분석 플로우차트 (출처: 공식 문서 및 벤치마크 데이터 기반)
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
# Preflight OPTIONS 요청 처리
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
# 실제 요청에 CORS 헤더 추가
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://backend:3000;
}
}
막히는 케이스 — CORS 에러 원인별 해결
CORS 에러는 유형이 몇 가지로 나뉜다. 에러 메시지를 정확히 읽으면 원인을 찾을 수 있다.
케이스 1 — credentials 포함 요청에서 * 사용 시 에러
현상: credentials: 'include'로 요청하면 CORS 에러 발생. 원인: Access-Control-Allow-Origin: *는 credentials와 함께 쓸 수 없다는 브라우저 규칙. 해결: Access-Control-Allow-Origin을 구체적인 출처로 지정하고, Access-Control-Allow-Credentials: true를 함께 설정한다.
케이스 2 — Preflight는 통과하지만 실제 요청이 막힘
현상: 네트워크 탭에서 OPTIONS 요청은 200인데 실제 요청은 CORS 에러. 원인: Preflight 응답에는 CORS 헤더가 있지만, 실제 응답에 Access-Control-Allow-Origin 헤더가 없는 경우. Nginx의 add_header는 기본적으로 2xx/3xx에만 적용된다. 해결: Nginx에서 add_header ... always를 사용하거나, 애플리케이션 레벨에서 모든 응답에 헤더를 추가한다.
케이스 3 — 개발 환경에서만 CORS 우회 프록시 사용
현상: 로컬에서는 프록시로 잘 되는데 프로덕션 배포 후 CORS 에러 발생. 원인: Create React App / Vite의 개발 서버 프록시 기능은 로컬에서만 동작한다. 프로덕션에서는 실제 CORS 헤더가 필요하다. 해결: 프로덕션 서버에서 올바른 CORS 헤더를 설정하거나, 같은 출처의 API Gateway를 통해 프록시한다.