CORS

Category
Criti AI
Status
Published
Tags
cors
Description
Published
Slug
MDN Web DocsMDN Web DocsCross-Origin Resource Sharing (CORS) - HTTP | MDN
 
CORS (Cross-Origin Resource Sharing, 교차 출처 리소스 공유)
"다른 출처(도메인)의 리소스를 안전하게 사용하기 위한 규칙(정책)"임

 

1. CORS는 왜 필요한가? - '동일 출처 정책' 때문

은행 앱을 예로 들면
  • 동일 출처 정책 (Same-Origin Policy, SOP): 웹 브라우저의 매우 중요한 보안 규칙
    • "A은행 사이트(https://a-bank.com)에서 실행된 스크립트는 A은행 서버의 데이터만 요청할 수 있다"는 원칙. 만약 이 규칙이 없다면, 악성 사이트(https://evil-site.com)에 접속했을 때 그 사이트의 스크립트가 멋대로 A은행 서버에 요청을 보내 계좌 정보를 훔쳐갈 수 있음
 
  • 문제의 시작: 하지만 현대 웹은 여러 출처의 리소스를 조합해 만드는 경우가 많음.
    • 예를 들어, 내 쇼핑몰(https://my-shop.com)에서 카카오 지도 API(https://dapi.kakao.com)를 가져와 지도를 보여주고 싶은데. 이때 '동일 출처 정책'에 따르면 내 쇼핑몰은 카카오의 리소스를 요청할 수 없어야 함
       
  • CORS의 역할: 이 문제를 해결하기 위해 CORS가 등장했음.
    • CORS는 서로 다른 출처 간에도 특정 조건 하에서는 리소스 공유를 허용해주는 '안전한 예외 규칙'
      즉, 카카오 서버가 "https://my-shop.com은 내 지도 데이터를 가져가도 괜찮아"라고 허락해주면 브라우저가 이를 확인하고 데이터를 정상적으로 보여주는 방식
       

2. CORS 동작 과정 - HTTP 헤더를 이용한 대화

CORS의 핵심은 브라우저와 서버가 HTTP 헤더를 통해 서로 대화하며 허락을 구하고 받는 과정임
 
마치 아파트에 택배 보내는 과정과 같음
  • 나 (브라우저): 다른 동네(Origin)에 사는 친구에게 택배를 보내려고 함
  • 택배 (HTTP 요청): GET, POST, PUT
  • 친구네 아파트 경비실 (서버): 보안을 담당
 
만약 내가 보내는 것이 간단한 편지(GET 요청 등)라면, 경비실은 그냥 통과시켜 줌. 이걸 "단순 요청(Simple Request)"이라고 함
하지만 내가 커다란 소포(PUT 요청, 커스텀 헤더 등)를 보낸다면, 경비실에서는 혹시 위험한 물건일까 봐 바로 들여보내 주지 않고, 친구에게 먼저 연락해서 확인함.
이 확인 전화가 바로 OPTIONS 예비 요청(Preflight Request)입니다.

 

가. 단순 요청 (Simple Requests)

GET, POST 같은 일부 간단한 요청은 한 번에 처리됨
  1. 브라우저 → 서버
      • 브라우저가 https://api.example.com으로 데이터를 요청하며, 요청 헤더에 자신의 출처를 담아 보냄
      • Origin: https://my-app.com
  1. 서버 → 브라우저
      • 서버는 요청을 받고, Origin 헤더를 확인함.
      • 만약 https://my-app.com요청을 허용한다면, 응답 헤더에 허락의 표시를 담아 보냄.
      • Access-Control-Allow-Origin: https://my-app.com
       
  1. 브라우저의 판단
      • 브라우저는 응답 헤더의 Access-Control-Allow-Origin 값을 보고 서버가 요청을 허락했는지 확인.
        • 값이 일치하면 데이터를 정상적으로 처리하고, 일치하지 않거나 헤더가 없으면 CORS 에러를 발생시키고 데이터를 폐기함
           

나. 프리플라이트 요청 (Preflight Requests)

PUT, DELETE 처럼 서버 데이터에 영향을 줄 수 있는 '위험한' 요청이나, 커스텀 헤더가 포함된 요청의 경우,
브라우저는 본 요청을 보내기 전에 "이런 요청을 보내도 괜찮을까요?" 라고 묻는 예비 요청(preflight request)을 먼저 보냄.
이 예비 요청은 OPTIONS라는 HTTP 메소드를 사용
 
  1. (예비) 브라우저 → 서버 (OPTIONS 요청)
      • 브라우저가 OPTIONS 메소드로 예비 요청을 보냄
      • 헤더에는 앞으로 보낼 본 요청에 대한 정보(사용할 메소드, 포함될 헤더 등)를 담음
      `OPTIONS /api/items/123 HTTP/1.1` `Origin: https://my-app.com` `Access-Control-Request-Method: PUT` (이제 PUT 요청 보낼 거다) `Access-Control-Request-Headers: Content-Type` (Content-Type 헤더를 사용할 거다)
      • OPTIONS /resource HTTP/1.1: resource라는 주소에 대해 질문(OPTIONS)이 있다.
      • Origin: https://my-app.com: 나는 my-app.com에서 출발했는데
      • Access-Control-Request-Method: PUT: 내가 보내고 싶어 하는 본 요청의 메소드는 PUT이다.
      • Access-Control-Request-Headers: Content-Type: 그리고 본 요청에 Content-Type 헤더를 포함할 예정이다
      OPTIONS는 이 요청 자체의 메소드이고, Access-Control-Request-Method는 앞으로 보낼 본 요청의 메소드를 알려주는, 질문의 내용
       
  1. (예비) 서버 → 브라우저
      • 서버는 이 예비 요청을 받고 자신이 허용하는 정책을 응답 헤더에 담아 알려줌.
      `HTTP/1.1 204 No Content` `Access-Control-Allow-Origin: https://my-app.com` `Access-Control-Allow-Methods: GET, POST, PUT, DELETE` (이 메소드들은 허용함) `Access-Control-Allow-Headers: Content-Type` (이 헤더는 사용해도 괜찮다)
      • https://my-app.com에서 오는 요청은 허용한다. PUT 메소드도 괜찮고, Content-Type 헤더를 사용하는 것도 허용
       
  1. 브라우저의 판단 및 본 요청
      • 브라우저는 서버의 허용 정책을 확인함. 자신이 보내려던 본 요청(PUT, Content-Type 헤더 사용)이 허용 범위에 포함되어 있으면, 그때서야 진짜 본 요청(PUT)을 서버로 보냄.
      • 만약 허용되지 않으면, CORS 에러를 발생시키고 본 요청은 아예 보내지 않음
       
      • 브라우저가 서버로부터 허락 응답을 받으면, 그제야 원래 보내려 했던 진짜 요청, 즉 PUT 요청을 보냄

 
  • SOP (동일 출처 정책): 기본적으로 다른 출처의 리소스 요청을 막는 브라우저의 보안 장치.
  • CORS (교차 출처 리소스 공유): SOP의 예외 규칙으로, 서버가 허락한 다른 출처에게만 리소스 접근을 허용하는 안전장치.
  • 동작 원리: 브라우저와 서버가 HTTP 헤더(Origin, Access-Control-Allow-Origin 등)를 통해 서로 소통하며 권한을 확인하는 방식
  • Preflight (OPTIONS 요청): 실제 데이터에 영향을 줄 수 있는 요청 전, 허락을 받기 위해 먼저 보내는 예비 질문
 
→ 웹 개발 시 CORS 에러가 발생하면 "브라우저가 동일 출처 정책에 따라 요청을 막았고, 목적지 서버가 허락(Access-Control-Allow-Origin 헤더)을 해주지 않았구나"라고 이해하면 됨
 

CORS의 핵심 동작 원리

 

브라우저의 자동화된 역할

CORS 정책을 확인하고 강제하는 주체는 브라우저. 개발자는 평소처럼 서버에 데이터 요청 코드(예: fetch, axios)를 작성하기만 하면 됨
→ 클라이언트 코드와 서버 사이에서 보안 미들웨어처럼 동작
 
정확한 처리 흐름
  1. 개발자 코드: fetch('https://api.example.com/data') 코드를 실행
    1.  
  1. 브라우저 가로채기 (자동): 요청이 실제로 네트워크로 전송되기 전에 브라우저가 이 요청을 가로챔
    1.  
  1. Origin 헤더 추가 (자동): 브라우저는 요청 헤더에 이 요청이 시작된 출처가 어디인지 알려주는 Origin 헤더를 자동으로 추가함. (예: Origin: https://my-app.com)
    1.  
  1. 서버 응답: 서버는 요청을 받고, 설정된 CORS 정책에 따라 응답 헤더에 Access-Control-Allow-Origin 등을 포함하여 응답함
    1.  
  1. 브라우저 검사 (자동): 응답이 개발자 코드에 도달하기 전에 브라우저가 먼저 응답 헤더를 검사
      • 허용: Access-Control-Allow-Origin 헤더 값이 Origin과 일치하면, 브라우저는 이 응답을 안전하다고 판단하고 개발자 코드에게 전달. fetch는 성공적으로 완료됨
      • 차단: 헤더가 없거나 값이 일치하지 않으면, 브라우저는 이 응답을 위험하다고 판단하고 데이터를 폐기한 뒤, 개발자 콘솔에 CORS 에러를 띄움
 
→ 이 모든 과정은 개발자가 모르는 사이에 브라우저 보안 엔진에 의해 자동으로 처리됨

 

프리플라이트 요청과 브라우저 '기억'

매번 본 요청 전에 예비 요청을 보내면 성능이 저하될 수밖에 없음
→ 브라우저는 이 예비 요청(Preflight)의 결과를 일정 시간 동안 '기억' (캐시, Cache)함
"나도 모르는 사이에 빠르게 왔다 갔다 하는" 과정은 최초 한 번만 (또는 캐시가 만료되었을 때) 일어남
 
  • Access-Control-Max-Age 헤더: 서버는 프리플라이트 요청에 대한 응답을 줄 때, 이 결과를 브라우저가 얼마 동안 기억해도 되는지 Access-Control-Max-Age 헤더에 초 단위로 명시할 수 있음
    • 예: Access-Control-Max-Age: 86400 (24시간 동안 기억해도 좋다는 의미)
    •  
프리플라이트 캐시 동작 시나리오
  1. 최초의 PUT 요청
      • 브라우저: "(예비) 이 도메인에 PUT 요청 보내도 되나요?" (OPTIONS 요청 전송)
      • 서버: "네, 허용합니다. 그리고 이 허락은 24시간 동안 유효합니다." (Access-Control-Max-Age: 86400 포함하여 응답)
      • 브라우저: "알겠습니다." (결과를 캐시에 저장)
      • 브라우저: "(본 요청) 데이터 여기 있습니다." (PUT 요청 전송)
       
  1. 5분 뒤, 두 번째 PUT 요청
      • 브라우저: (자신의 캐시를 확인) "24시간 안 지났고, 이미 허락받았으니 또 물어볼 필요 없다."
      • 브라우저: "(본 요청) 데이터 또 보냄" (예비 요청 없이 바로 PUT 요청 전송)
       
개발자가 모르는 사이에 OPTIONS 요청이 왔다 갔다 하지만,
성능 저하를 막기 위해 브라우저가 서버의 허락 하에 그 결과를 똑똑하게 기억하고 재사용함