🛑 CORS 오류 원리 완벽 해설: 브라우저 보안의 필수 관문! 🛡️

웹 개발을 하다 보면 누구나 한 번쯤 붉은색 글씨의 CORS 오류(Cross-Origin Resource Sharing Error)를 만나게 됩니다. 이 오류는 코딩 실수가 아니라, 사실은 사용자의 보안을 지키기 위한 웹 브라우저의 ‘착한 방어 행동’입니다.

이 글에서는 CORS 오류가 발생하는 이유와 원리를 초보자 눈높이에 맞춰 쉽게, 그리고 보안 측면에서 깊이 있게 설명해 드릴게요.


1. 🏠 동일 출처 정책 (Same-Origin Policy, SOP)의 탄생

CORS를 이해하려면 먼저 웹 보안의 가장 기본이 되는 규칙인 동일 출처 정책(SOP)부터 알아야 합니다.

1) 동일 출처(Origin)란 무엇인가?

브라우저는 웹사이트의 주소(URL)를 다음 세 가지 요소로 구분합니다. 이 세 가지가 모두 같아야 ‘동일 출처(Same-Origin)’로 인정됩니다.

구성 요소예시
프로토콜http: vs https:
도메인 (호스트)api.google.com vs www.google.com
포트 번호80 vs 3000
URL AURL B동일 출처 여부이유
https://a.com:443https://a.com:443✅ 동일모두 같음
https://a.comhttp://a.com❌ 다름프로토콜 (https vs http) 다름
https://a.com:8080https://a.com❌ 다름포트 번호 (8080 vs 443 또는 생략) 다름

2) SOP의 역할: 악성 사이트 차단

SOP는 “다른 출처에서 가져온 데이터에는 함부로 접근할 수 없다“는 규칙입니다.

만약 SOP가 없다면 어떤 일이 생길까요?

  • 사용자가 금융 사이트(bank.com)에 로그인한 상태에서, 동시에 악성 사이트(hacker.com)를 열어봅니다.
  • hacker.com에 있는 자바스크립트 코드가 bank.com으로 요청을 보내서 사용자의 쿠키나 민감 정보를 훔쳐갈 수 있게 됩니다.

SOP는 바로 이런 악의적인 데이터 탈취 행위를 브라우저 레벨에서 원천적으로 차단합니다.


2. 🚦 CORS (Cross-Origin Resource Sharing): 예외 허용

SOP는 보안을 위해 훌륭하지만, 현실적으로는 다른 출처 간의 데이터 통신이 필수적입니다.

  • 프론트엔드 (React, Vue) 서버: http://localhost:3000
  • 백엔드 (API) 서버: http://api.myapp.com

이 두 출처는 명백히 다릅니다. 이때 CORS는 SOP 원칙을 유지하면서도, 서버가 명시적으로 허용한 경우에 한해 다른 출처의 요청을 받아주도록 하는 메커니즘입니다.

1) CORS 오류가 발생하는 이유

  1. 프론트엔드(http://localhost:3000)가 백엔드(http://api.myapp.com)로 데이터를 요청합니다.
  2. 요청은 서버에 정상적으로 전달되고, 서버도 응답 데이터를 정상적으로 생성합니다.
  3. 문제 발생: 응답이 클라이언트(브라우저)에 도착했을 때, 브라우저가 보안 검사를 수행합니다.
  4. 브라우저는 이 응답이 ‘다른 출처’에서 왔음을 확인하고, 서버가 이 출처의 접근을 허용했는지 확인합니다.
  5. 만약 서버의 응답 헤더에 “너의 출처는 허용되지 않았어!”라는 내용이 담겨있다면, 브라우저는 해당 응답을 자바스크립트 코드에서 사용하지 못하도록 막아버립니다. (이것이 빨간색 CORS 오류입니다.)

2) 서버가 보내야 하는 핵심 응답 헤더

CORS 문제를 해결하려면, API 서버가 응답 헤더에 다음 내용을 반드시 포함시켜야 합니다.

  • Access-Control-Allow-Origin: [허용할 출처]
    • 예시: Access-Control-Allow-Origin: http://localhost:3000
    • 모든 출처를 허용하려면: Access-Control-Allow-Origin: * (보안상 권장되지 않음)

3. ✈️ Preflight Request (프리플라이트 요청)의 원리

CORS 통신에는 ‘일반 요청’ 외에 보안 강화를 위한 특별한 절차가 있습니다. 바로 Preflight Request(사전 요청)입니다.

1) 왜 Preflight가 필요할까?

일부 HTTP 메서드(예: PUT, DELETE)나 특정 커스텀 헤더를 사용하는 요청은 잠재적으로 서버 데이터를 변경하거나 민감할 수 있습니다. 브라우저는 이 요청을 실제로 보내기 전에, “이 요청을 보내도 될까요?”라고 서버에게 먼저 물어봅니다.

2) Preflight 동작 순서

  1. 브라우저 요청: 실제 요청(예: PUT 요청)을 보내기 직전에, 브라우저는 먼저 서버에 OPTIONS 메서드로 요청을 보냅니다. (이것이 Preflight Request입니다.)
  2. 서버 응답: 서버는 이 OPTIONS 요청에 대한 응답으로 “이런 메서드와 이런 헤더를 가진 요청은 우리 서버가 받아줄 수 있어.” 라는 정보를 응답 헤더에 담아 보냅니다.
  3. 브라우저 검증: 브라우저는 서버의 응답을 보고, 실제 보낼 요청의 조건이 서버의 허용 조건과 일치하는지 확인합니다.
  4. 실제 요청 전송: 검증에 성공하면, 브라우저는 비로소 실제 PUT 요청을 서버에 전송합니다.
서버가 응답해야 하는 헤더의미
Access-Control-Allow-Origin어떤 출처를 허용할 것인가?
Access-Control-Allow-MethodsGET, POST, PUT 중 어떤 HTTP 메서드를 허용할 것인가?
Access-Control-Allow-HeadersContent-Type, Authorization 등 어떤 헤더를 허용할 것인가?

4. 🔑 결론: CORS는 서버 설정 문제!

CORS 오류는 “프론트엔드 코드가 잘못되었다”는 의미가 아닙니다.

CORS 오류는 “백엔드 서버가 프론트엔드 출처로부터의 접근을 허용하겠다는 명시적인 약속(헤더 설정)을 브라우저에게 보내지 않았다”는 의미입니다.