CORS는 누가, 왜 막는 것인가
CORS 오류의 첫 오해는 "서버가 요청을 거부했다"입니다. 실제로는 브라우저가 응답을 자바스크립트에 전달하지 않는 것입니다. 브라우저는 기본적으로 다른 출처(origin: 프로토콜+도메인+포트 조합)의 응답을 스크립트가 읽지 못하게 막는 동일 출처 정책(Same-Origin Policy)을 적용하고, CORS는 서버가 "이 출처는 읽어도 된다"고 예외를 선언하는 메커니즘입니다.
그래서 같은 요청이 curl이나 Postman에서는 성공하고 브라우저에서만 실패합니다 — 정책의 주체가 브라우저이기 때문입니다. 해결 위치도 항상 서버의 응답 헤더입니다.
오류 메시지 3종 해석
| 콘솔 메시지 (요지) | 의미 | 해결 방향 |
|---|---|---|
No 'Access-Control-Allow-Origin' header | 서버가 CORS 허용을 선언하지 않음 | 서버 응답에 해당 헤더 추가 |
Response to preflight request doesn't pass | 본 요청 전 OPTIONS 예비 요청이 거부됨 | 서버가 OPTIONS 메서드에 200 + CORS 헤더로 응답하도록 수정 |
The value ... must not be the wildcard '*' when credentials mode is 'include' | 쿠키 포함 요청에 와일드카드 사용 | Allow-Origin에 구체적 출처 명시 + Allow-Credentials: true |
preflight(예비 요청)는 언제 발생하나
단순 요청(GET/POST + 표준 헤더 + 일반 콘텐츠 타입)은 바로 전송되지만, 다음 중 하나라도 해당하면 브라우저가 먼저 OPTIONS 요청을 보내 허가를 확인합니다.
Content-Type: application/json으로 보내는 경우 (실무 API 대부분이 여기 해당)Authorization등 커스텀 헤더를 붙이는 경우- PUT, DELETE, PATCH 메서드를 쓰는 경우
즉, JSON API에 토큰 인증을 쓰는 순간 preflight는 사실상 항상 발생합니다. OPTIONS 요청이 401을 받지 않도록, 인증 미들웨어보다 앞에서 처리해야 한다는 점이 서버 구현에서 가장 자주 놓치는 부분입니다.
서버 응답 헤더 구성
| 헤더 | 예시 값 | 역할 |
|---|---|---|
| Access-Control-Allow-Origin | https://app.example.com | 허용 출처 (credentials 사용 시 * 불가) |
| Access-Control-Allow-Methods | GET, POST, PUT, DELETE | preflight 응답에서 허용 메서드 선언 |
| Access-Control-Allow-Headers | Content-Type, Authorization | 클라이언트가 보낼 수 있는 헤더 |
| Access-Control-Allow-Credentials | true | 쿠키·인증정보 포함 허용 |
| Access-Control-Max-Age | 86400 | preflight 결과 캐시 (OPTIONS 트래픽 감소) |
응답에 이 헤더들이 실제로 실려 오는지는 HTTP 헤더 분석기로 확인할 수 있습니다. 개발자 도구 Network 탭에서 OPTIONS 요청을 필터링해 상태 코드와 응답 헤더를 보는 것도 같은 진단입니다.
하지 말아야 할 해결책
- 무조건
*허용 — 공개 API가 아니라면 출처를 명시하세요. 쿠키 인증과는 아예 조합이 불가능합니다. - 브라우저 보안 플래그 끄기 — 개발 편의로
--disable-web-security를 쓰면 그 브라우저의 모든 사이트 보안이 꺼집니다. 팀에 공유되는 순간 사고입니다. - 정체불명 CORS 프록시 경유 — 요청·응답 전문이 제3자 서버를 통과합니다. 인증 토큰이 있는 요청이라면 자격 증명을 통째로 넘기는 셈입니다.
개발 단계의 올바른 우회는 프론트엔드 개발 서버(Vite, webpack-dev-server)의 프록시 설정으로 같은 출처처럼 보이게 만드는 것, 운영에서는 서버에 정식으로 CORS 헤더를 구성하는 것입니다. 보안 헤더 전반은 HTTP 헤더 완전 가이드에서 다룹니다.
자주 묻는 질문
Postman에서는 되는데 브라우저에서만 실패하는 이유는?
동일 출처 정책은 브라우저의 보안 모델이기 때문입니다. Postman·curl은 이 정책의 적용 대상이 아니므로, 이 차이 자체가 "CORS 문제"라는 진단 근거가 됩니다.
CORS 오류인데 서버 로그에는 요청이 정상 처리로 남습니다. 왜죠?
서버는 응답을 정상적으로 보냈고, 브라우저가 그 응답을 스크립트에 전달하지 않은 것입니다. 차단 시점이 응답 수신 후라는 것이 CORS의 특징입니다.
localhost에서 개발할 때도 CORS가 걸리나요?
네. localhost:3000과 localhost:8080은 포트가 달라 다른 출처입니다. 개발 서버 프록시 설정이 표준 해법입니다.