TechFeedTechFeed
Security

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-AgePreflight 결과 캐시 시간(초)86400
Access-Control-Expose-HeadersJS에서 접근 허용할 응답 헤더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 에러는 유형이 몇 가지로 나뉜다. 에러 메시지를 정확히 읽으면 원인을 찾을 수 있다.

케이스 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를 통해 프록시한다.
CORS보안웹개발ExpressNginx

관련 포스트

OWASP Top 10 2026 — 웹 보안 필수 체크리스트2026-02-18인증 구현 가이드 2026 — JWT, OAuth, Passkey2026-02-20API 보안 체크리스트 20262026-03-06JWT vs 세션 인증 — 무엇을 선택할 것인가2026-03-07