본문으로 바로가기

CORS — 브라우저는 왜 그 요청을 막았나

같은 출처, 허용된 교차 출처, 헤더 없는 차단, 사전 요청(preflight) 네 장면을 실행해 'CORS는 서버가 아니라 브라우저의 규칙'임을 보여준다.

1 / 7app.example.com의 SPA가 api.other.com을 부르려 합니다. CORS는 '서버의 방화벽'이 아니라 '브라우저가 JS에게 응답을 보여줄지'의 규칙입니다.
서버 처리: JS 수신:
단계 목록 · 키보드 ←/→ 이동, Space 재생

CORS 에러가 나도 요청은 이미 서버에 도착해 처리됐을 수 있다 — 차단되는 것은 '응답을 JS에게 보여주는 일'이고, 그 문지기는 서버가 아니라 브라우저다.

왜 헷갈리는가

"CORS 에러 = 서버 방화벽이 막음"이 최대 오해다. 그래서 프론트엔드에서 고치려 하거나(불가능), 서버 로그에 요청이 찍히는데 왜 에러냐고 혼란해한다. 진실: 브라우저가 응답의 허가 헤더를 보고 JS에게 감춘 것이다.

애니메이션이 보여주는 것

세 번째 장면이 핵심이다 — 서버는 200을 돌려줬고 '서버 처리: 됨'인데 'JS 수신: 차단됨'이다. 응답에 Access-Control-Allow-Origin이 없으면 브라우저는 받은 응답을 버린다. GET이었다면 서버 작업은 이미 끝난 뒤다.

네 번째 장면의 사전 요청은 그 보완이다. PUT+JSON처럼 '단순 요청'(GET/HEAD/POST + 안전한 헤더)을 벗어나면, 브라우저가 본 요청 전에 OPTIONS로 허락을 구한다 — 부작용이 있을 요청은 서버가 명시적으로 승인해야만 나간다. 승인(Max-Age) 동안은 정찰을 생략한다.

왜 이런 규칙이 있는가

브라우저에는 당신의 은행 쿠키가 들어 있다. 악성 사이트의 JS가 bank.com API를 마음대로 부르고 응답까지 읽을 수 있다면 로그인된 계좌가 그대로 털린다. 동일 출처 정책(SOP)이 그것을 막고, CORS는 '서버가 명시적으로 허락한 출처만' 구멍을 여는 통제된 완화 장치다.

그래서 CORS는 브라우저 밖에는 존재하지 않는다 — curl, 모바일 앱, 서버 간 호출에는 아무 제약이 없다. CORS를 보안 방화벽으로 믿으면 안 되는 이유이자, '프록시를 거치면 CORS가 사라지는' 이유다.

실무 처방전

에러 메시지별 원인은 거의 정해져 있다.

  • 'No Access-Control-Allow-Origin': 서버가 헤더를 안 줌 — 서버(또는 게이트웨이) 설정에서 출처를 허용 목록에 추가. 프론트에서 고칠 방법은 없다.
  • 'Response to preflight …': OPTIONS가 401/404로 죽는 경우 — 인증 미들웨어가 OPTIONS까지 잠갔는지, 라우터가 OPTIONS를 받는지 확인.
  • 쿠키가 안 실림: fetch에 credentials:'include' + 서버에 Allow-Credentials:true, 그리고 이때 Allow-Origin은 *가 아니라 정확한 출처여야 한다.
  • 개발 중 임시 우회: dev 서버 프록시(vite/next의 proxy)로 같은 출처로 만들기 — 브라우저를 끄는 확장은 팀 전체를 속인다.

기억할 것

  • CORS는 브라우저의 규칙 — 서버 간·curl에는 존재하지 않는다.
  • 차단돼도 요청은 서버에 닿았을 수 있다. 차단되는 것은 JS의 '읽기'다.
  • 위험한 교차 요청은 OPTIONS 사전 요청으로 먼저 허락을 구한다.
  • 고치는 곳은 언제나 서버의 응답 헤더다 — Allow-Origin·Methods·Headers·Credentials.
CORS — 브라우저는 왜 그 요청을 막았나 | KIE