CORS 완벽 이해 — 왜 차단되고 어떻게 해결하나
Same-Origin Policy부터 CORS 헤더, Preflight 요청, Express/Nginx 설정까지 실전 해결 가이드.
한 줄 요약: 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 관련 헤더를 포함해야 한다. 주요 헤더와 역할은 다음과 같다.
| 헤더 | 설명 | 예시 값 |
|---|---|---|
| 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 |
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 레벨은 여러 백엔드를 동시에 처리할 때 유리하다. 둘 다 설정하면 헤더가 중복될 수 있으니 한 레벨에서만 처리하는 것을 권장한다.
Express — cors 패키지 사용 (환경별 출처 제어)const cors = require('cors'); const allowedOrigins = [ 'https://example.com', 'https://www.example.com', ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []) ]; const corsOptions = { origin: (origin, callback) => { // origin이 없으면 서버-서버 요청 (Postman 포함) — 허용 if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('CORS policy violation')); } }, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, // 쿠키/인증 헤더 허용 시 maxAge: 86400 // Preflight 캐시 24시간 }; app.use(cors(corsOptions)); // OPTIONS Preflight 처리 app.options('*', cors(corsOptions));
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 에러는 유형이 몇 가지로 나뉜다. 에러 메시지를 정확히 읽으면 원인을 찾을 수 있다.
현상:
credentials: 'include'로 요청하면 CORS 에러 발생.원인:
Access-Control-Allow-Origin: *는 credentials와 함께 쓸 수 없다는 브라우저 규칙.해결:
Access-Control-Allow-Origin을 구체적인 출처로 지정하고, Access-Control-Allow-Credentials: true를 함께 설정한다.현상: 네트워크 탭에서 OPTIONS 요청은 200인데 실제 요청은 CORS 에러.
원인: Preflight 응답에는 CORS 헤더가 있지만, 실제 응답에
Access-Control-Allow-Origin 헤더가 없는 경우. Nginx의 add_header는 기본적으로 2xx/3xx에만 적용된다.해결: Nginx에서
add_header ... always를 사용하거나, 애플리케이션 레벨에서 모든 응답에 헤더를 추가한다.현상: 로컬에서는 프록시로 잘 되는데 프로덕션 배포 후 CORS 에러 발생.
원인: Create React App / Vite의 개발 서버 프록시 기능은 로컬에서만 동작한다. 프로덕션에서는 실제 CORS 헤더가 필요하다.
해결: 프로덕션 서버에서 올바른 CORS 헤더를 설정하거나, 같은 출처의 API Gateway를 통해 프록시한다.