CORS 에러 완전 정리 — 교차 출처 차단 원인·프리플라이트·서버별 해결
프론트엔드에서 API를 호출했더니 콘솔에 빨갛게 "blocked by CORS policy"가 뜬다. 분명 서버는 정상이고 Postman으로는 잘 되는데 브라우저에서만 막힌다. CORS는 웹 개발자라면 누구나 한 번은 부딪히는 벽이다. 핵심을 먼저 말하면, CORS 에러는 서버가 죽은 게 아니라 브라우저가 "이 응답을 자바스크립트가 읽어도 되는지" 확인하지 못해 가로막은 것이다. 이 글은 CORS가 왜 존재하는지, 오리진과 프리플라이트가 무엇인지, 그리고 서버에서 어떻게 푸는지를 코드와 함께 정리한다.

이 글의 구성
🔎 CORS란 무엇인가
CORS는 Cross-Origin Resource Sharing, 교차 출처 자원 공유다. 이름은 거창하지만 출발점은 단순한 규칙 하나다. 브라우저는 기본적으로 같은 출처(same-origin)끼리만 자유롭게 데이터를 주고받게 한다. 이걸 동일 출처 정책(Same-Origin Policy)이라 부른다. CORS는 이 빗장을 "정해진 조건에서만" 풀어 주는 표준이다.
출처(origin)는 세 가지로 정해진다. 프로토콜(http/https), 호스트(도메인), 포트다. 셋 중 하나라도 다르면 다른 출처다. 예를 들어 https://app.example.com에서 https://api.example.com을 부르면, 도메인이 달라 교차 출처다. 포트만 8080으로 달라도 교차 출처다. 이때 브라우저는 서버가 "그쪽에서 읽어도 돼"라고 허락하는 헤더를 보냈는지 확인하고, 없으면 자바스크립트가 응답을 읽지 못하게 막는다.
핵심 — 막는 주체는 브라우저다
CORS는 서버가 아니라 브라우저가 강제하는 규칙이다. 그래서 같은 요청을 curl이나 Postman으로 보내면 멀쩡히 응답이 온다. 거기엔 동일 출처 정책이 없기 때문이다. 브라우저 안의 자바스크립트만 이 규칙에 묶인다. CORS 에러를 "서버가 죽었나" 하고 의심하면 방향이 틀어진다.
🧩 에러가 뜨는 진짜 이유
CORS 에러의 가장 흔한 오해는 "요청이 서버에 도달조차 못 했다"는 생각이다. 실제로는 많은 경우 요청이 서버에 닿고, 서버가 응답까지 한다. 다만 그 응답에 "이 출처가 읽어도 된다"는 허락 헤더(Access-Control-Allow-Origin)가 없어서, 브라우저가 응답을 자바스크립트에 넘기지 않고 막는 것이다.
즉 CORS 에러는 "통신 실패"가 아니라 "읽기 권한 거부"에 가깝다. 서버 로그를 보면 200으로 정상 응답한 기록이 남아 있는데 프론트에서만 에러가 나는 이유가 여기에 있다.
에러가 나는 대표 상황
🔄 프리플라이트(OPTIONS) 동작
CORS에는 두 종류의 요청이 있다. 단순 요청(simple request)과 프리플라이트가 필요한 요청이다. 이 구분을 알면 "왜 OPTIONS 요청이 하나 더 날아가지?" 하는 의문이 풀린다.
단순 요청은 GET·POST·HEAD에, 헤더가 평범하고, Content-Type이 application/x-www-form-urlencoded·multipart/form-data·text/plain 중 하나인 경우다. 이건 브라우저가 바로 본 요청을 보내고, 응답 헤더로 허용 여부를 따진다.
그 조건을 벗어나면, 예를 들어 PUT·DELETE 메서드나 application/json 본문, Authorization 같은 커스텀 헤더를 쓰면, 브라우저는 본 요청 전에 OPTIONS 메서드로 "예비 질문"을 먼저 보낸다. 이게 프리플라이트다. "나 이런 메서드와 헤더로 요청하려는데 괜찮아?"라고 묻고, 서버가 허용 메서드·헤더를 응답하면 그제야 본 요청을 보낸다.
| 단계 | 요청 헤더 | 응답 헤더(서버) |
|---|---|---|
| 프리플라이트(OPTIONS) | Access-Control-Request-Method / -Headers | Access-Control-Allow-Methods / -Headers |
| 본 요청 | Origin | Access-Control-Allow-Origin |
프리플라이트에서 막히는 경우가 의외로 많다. 서버가 OPTIONS 메서드를 처리하지 않아 404·405를 돌려주거나, 허용 헤더 목록에 Authorization을 안 넣었으면 본 요청은 시작도 못 한다. 그래서 CORS 디버깅은 OPTIONS 응답부터 보는 게 빠르다.
💻 실제 에러 메시지와 확인
브라우저 콘솔에 찍히는 메시지는 원인을 꽤 정확히 알려준다. 대표적인 문구다.
# 허용 헤더 자체가 없음 (가장 흔함)
Access to fetch at 'https://api.example.com' from origin
'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present.
# 쿠키를 보내는데 출처가 * 로 돼 있음
The value of the 'Access-Control-Allow-Origin' header must not
be the wildcard '*' when the request's credentials mode is 'include'.
서버가 어떤 CORS 헤더를 주는지는 curl로 프리플라이트를 흉내 내 보면 바로 보인다. Origin과 요청 메서드를 실어 OPTIONS를 보낸다.
# 프리플라이트 흉내 — 응답의 Allow-* 헤더 확인
curl -i -X OPTIONS https://api.example.com/data \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST'
# ✓ 정상이면 응답에 이런 헤더가 보인다
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, POST, PUT, DELETE
개발자도구 네트워크 탭에서도 본 요청 앞에 OPTIONS 요청이 따로 있는지, 그 응답에 Allow 헤더가 붙었는지 확인할 수 있다.
🛠 서버별 해결 방법
CORS는 응답 헤더로 푸는 문제이므로, 고칠 곳은 거의 항상 서버다. 대표 환경별 설정을 본다. Node.js Express에서는 cors 미들웨어가 가장 간단하다.
// Express — cors 미들웨어
const cors = require('cors');
app.use(cors({
origin: 'https://app.example.com', // 특정 출처만 허용
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // 쿠키 허용 시
}));
Nginx에서는 add_header로 헤더를 붙이고, OPTIONS 프리플라이트를 따로 처리해 준다.
# Nginx — CORS 헤더 + OPTIONS 처리
# 주의: 헤더를 OPTIONS 블록 "안"에도 직접 넣어야 한다.
# 바깥 add_header 만 두고 return 204 하면 헤더가 빠지는 함정.
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin https://app.example.com always;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE' always;
add_header Access-Control-Allow-Headers 'Content-Type, Authorization' always;
add_header Access-Control-Max-Age 86400 always;
add_header Content-Length 0;
return 204; # 프리플라이트는 본문 없이 204
}
add_header Access-Control-Allow-Origin https://app.example.com always;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE' always;
add_header Access-Control-Allow-Headers 'Content-Type, Authorization' always;
# ... 본 요청 처리 ...
}
쿠키·토큰 같은 인증을 함께 보낸다면 규칙이 하나 더 붙는다. 서버는 Access-Control-Allow-Credentials를 true로 줘야 하고, 이때 Allow-Origin뿐 아니라 Allow-Methods·Allow-Headers도 *를 쓸 수 없다. 인증 모드에서는 *가 와일드카드가 아니라 글자 그대로 해석되기 때문이다. 반드시 구체적인 값을 지정해야 한다. 프론트도 fetch에 credentials: 'include'를 넣어야 쿠키가 실린다. 한 가지 더, Authorization 헤더는 인증 여부와 무관하게 Allow-Headers의 *에 포함되지 않으므로 항상 이름을 명시해야 한다.
단계별 해결 순서
⚖️ 흔한 오해와 함정
CORS는 개념이 헷갈려 잘못 알기 쉽다. 자주 빠지는 함정을 정리했다.
| 오해 | 실제 |
|---|---|
| 프론트 코드로 고칠 수 있다 | 응답 헤더 문제라 서버에서 고친다. 프론트는 프록시 우회만 가능 |
| CORS가 서버를 보호한다 | 서버 보호가 아니라, 브라우저 안 사용자 데이터를 지키는 장치 |
| *로 다 열면 편하다 | 인증 요청엔 * 사용 불가, 보안상 구체 출처 지정 권장 |
| OPTIONS는 내가 안 보냈다 | 프리플라이트는 브라우저가 자동 생성. 서버가 처리해야 함 |
특히 "CORS는 서버를 공격으로부터 막아 준다"는 오해가 흔하다. CORS는 오히려 동일 출처 정책을 완화하는 장치다. 서버는 curl·다른 서버에서 얼마든지 접근할 수 있고, CORS가 지키는 건 브라우저 안에서 사용자의 자격 증명으로 다른 사이트가 몰래 요청을 읽어가지 못하게 하는 부분이다. 보안 경계를 서버 보호로 착각하면 설정을 엉뚱하게 한다.
CORS 에러는 서버가 죽은 게 아니라 브라우저가 응답을 못 읽게 막은 것이다. 그래서 답은 거의 항상 서버 응답 헤더에 있다.
— Postman은 되는데 브라우저만 막힌다면
💬 자주 묻는 질문 5가지
Q1Postman은 되는데 브라우저만 막혀요.
정상입니다. CORS는 브라우저만 강제하는 규칙이라, Postman·curl에는 동일 출처 정책이 없어 그대로 통과합니다. 서버가 죽은 게 아니라 응답 헤더가 빠진 것이니, 서버에 Access-Control-Allow-Origin을 추가하면 됩니다.
Q2프론트엔드에서 CORS를 고칠 수 없나요?
근본적으로는 못 고칩니다. 허용은 서버 응답 헤더로 결정되기 때문입니다. 다만 개발 단계에서는 dev 서버 프록시로 요청을 같은 출처처럼 우회하거나, 운영에서 같은 도메인 뒤로 API를 붙이는 식으로 교차 출처 자체를 없앨 수 있습니다.
Q3OPTIONS 요청이 왜 하나 더 날아가나요?
프리플라이트입니다. PUT·DELETE나 application/json, Authorization 같은 비단순 요청은 브라우저가 본 요청 전에 OPTIONS로 "이렇게 보내도 돼?"를 먼저 묻습니다. 서버가 허용 메서드·헤더로 답해야 본 요청이 진행됩니다.
Q4그냥 * 로 다 열면 안 되나요?
단순 공개 API라면 가능하지만, 쿠키·토큰 같은 인증을 보낼 때는 *를 쓸 수 없습니다. Allow-Credentials: true와 함께 쓰려면 반드시 구체적인 출처를 지정해야 합니다. 보안상으로도 허용 출처는 필요한 도메인만 명시하는 편이 안전합니다.
Q5헤더를 추가했는데도 계속 막혀요.
프리플라이트 응답을 확인하세요. 본 요청엔 헤더가 붙었는데 OPTIONS 응답에는 빠진 경우가 흔합니다. Allow-Headers에 Authorization 같은 실제 사용 헤더가 들어 있는지, OPTIONS가 200·204로 정상 응답하는지 curl로 점검합니다. 캐시된 프리플라이트가 남아 있으면 강력 새로고침도 시도하세요.
📌 결론
CORS 에러는 브라우저가 "이 교차 출처 응답을 자바스크립트가 읽어도 되는지" 확인하지 못해 막는 현상이다. 서버가 죽은 게 아니라, 응답에 허락 헤더가 없는 것이다. 그래서 해결의 무게중심은 늘 서버 응답 헤더에 있다.
실무 원칙은 다음과 같다.
🌐 출처 개념을 잡는다. 프로토콜·호스트·포트 중 하나라도 다르면 교차 출처이고, 브라우저가 허락 헤더를 따진다.
🔑 서버에서 헤더를 정확히 준다. Allow-Origin·Methods·Headers를 맞추고, 비단순 요청은 OPTIONS 프리플라이트까지 처리한다.
🛠 인증과 함정을 챙긴다. 쿠키를 쓰면 Allow-Credentials: true + 구체 출처(별표 금지) + 프론트 credentials: include, 개발 단계는 프록시로 우회한다.
CORS 해결 체크리스트
본 글은 브라우저 CORS 정책과 일반적 해결 방법을 정리한 자료다. 보안 설정 변경은 영향 범위를 확인한 뒤 신중히 적용한다.
#CORS #CORS에러 #교차출처 #SameOriginPolicy #프리플라이트 #preflight #AccessControlAllowOrigin #브라우저보안 #웹개발 #Express #Nginx #API #프론트엔드 #백엔드 #CORS해결
'IT' 카테고리의 다른 글
| HTTP 413 Payload Too Large — 요청 본문 초과 원인·해결·예방 (0) | 2026.06.28 |
|---|---|
| HTTP 405 Method Not Allowed — 메서드 허용 안 됨 원인·해결·예방 (0) | 2026.06.28 |
| HTTP 304 Not Modified — 캐시 검증·조건부 요청 동작과 디버깅 (0) | 2026.06.28 |
| 리다이렉트 301·302·307·308 차이 — 영구·임시·메서드 보존과 SEO (0) | 2026.06.28 |
| "이 사이트에 연결할 수 없음" 종합 자가진단 — 원인 찾기·해결 (0) | 2026.06.25 |
댓글