CORS 에러, 도대체 왜 나는 걸까: 원리와 해결
"Access to fetch ... has been blocked by CORS policy." 프론트 개발자라면 누구나 만나는 빨간 에러입니다. 원리를 알면 해결이 쉬워집니다.
CORS는 브라우저의 보안 장치다
브라우저는 **동일 출처 정책(Same-Origin Policy)**을 따릅니다. 출처(스킴+호스트+포트)가 다른 곳으로의 요청은 기본적으로 제한됩니다.
https://myapp.com → https://api.other.com # 다른 출처(cross-origin)
이걸 안전하게 허용하는 규약이 CORS입니다. 핵심: CORS는 서버가 허락해야 풀립니다. 프론트 코드만으로는 못 뚫습니다.
서버가 허락 헤더를 보내야 한다
서버가 응답에 다음 헤더를 넣어야 브라우저가 통과시킵니다.
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Allow-Origin은 정확한 출처를 넣는 게 안전합니다. *는 누구나 허용이라 인증 쿠키가 있는 요청에는 못 씁니다.
preflight: 진짜 요청 전의 사전 확인
PUT/DELETE이거나 커스텀 헤더(Authorization 등)가 있으면, 브라우저가 먼저 OPTIONS 요청을 보내 "이 요청 해도 돼?"를 묻습니다. 이게 preflight입니다.
브라우저 → OPTIONS /orders (preflight)
서버 → 200 + Access-Control-Allow-* 헤더
브라우저 → 실제 POST /orders
서버가 OPTIONS에 올바른 헤더로 답하지 않으면 본 요청은 시작도 못 합니다. CORS 에러의 상당수가 여기서 발생합니다.
쿠키를 함께 보낼 때
인증 쿠키를 보내려면 양쪽 설정이 모두 필요합니다.
fetch(url, { credentials: "include" }); // 프론트
Access-Control-Allow-Credentials: true # 서버
Access-Control-Allow-Origin: https://myapp.com # 이때 *는 불가
흔한 오해와 해결
- "프론트에서 헤더를 추가하면 되지 않나?" → 아닙니다. 허용은 서버가 합니다.
- 로컬에서만 안 됨 → 개발 중엔 프록시(예: 프레임워크 dev 서버의 rewrites)로 같은 출처처럼 우회
- 에러는 나는데 서버 로그엔 요청이 없음 → preflight(
OPTIONS)에서 막힌 것.OPTIONS핸들링 확인
마무리 체크리스트
- CORS는 서버가 허락하는 규약
- 복잡한 요청은
OPTIONSpreflight가 먼저 Allow-Origin은 정확한 출처, 쿠키 쓰면*금지- 로컬은 프록시로 우회
"누가 허락하는가? → 서버"만 기억하면 CORS 디버깅의 방향이 잡힙니다.
함께 보면 좋은 글
REST API 설계 원칙: 실무에서 욕먹지 않는 7가지 규칙
URL에 동사를 넣고, 200으로 에러를 내보내는 잘못된 API는 이제 그만. 리소스 명명, HTTP 메서드, 상태 코드, 버저닝까지 실무 표준에 맞춰 REST API를 설계하는 법을 정리했습니다.
HTTP 상태 코드 한눈에: 200·301·401·403·404·500의 진짜 의미
2xx·3xx·4xx·5xx의 큰 그림과, 실무에서 헷갈리는 401 vs 403, 301 vs 302, 200 vs 204의 차이를 API 설계 관점에서 정리합니다.
환경 변수와 비밀값 관리: .env를 커밋하면 안 되는 이유
.env로 설정을 분리하는 이유, .gitignore와 .env.example 패턴, NEXT_PUBLIC_ 접두사의 노출 위험, 키가 유출됐을 때 대처까지 정리합니다.