클라우드 기반

[웹 관련] CORS 에러, 더 이상 두렵지 않아! Simple vs Preflight

magnate96 2025. 6. 26. 16:05

웹 프론트엔드 개발을 하다가 API를 호출했는데, 브라우저 콘솔에 빨간 글씨로 나타나는 공포의 메시지를 본 적 있으신가요?

ACCESS TO FETCH AT 'HTTPS://API.EXAMPLE.COM/DATA' FROM ORIGIN 'HTTPS://MY-AWESOME-SITE.COM' HAS BEEN BLOCKED BY CORS POLICY...

이 CORS 에러는 우리를 좌절하게 만들지만, 사실은 우리를 지켜주는 고마운 '보안 요원'입니다. 이 보안 요원이 어떻게 일하는지, 그리고 Simple 요청과 Preflight 요청은 무엇인지 제대로 이해하면 더 이상 CORS 에러가 두렵지 않을 겁니다.

 

시작하기 전에: CORS는 왜 필요할까? - SOP 이야기

 

CORS를 이해하려면 먼저 브라우저의 기본 보안 정책인 **'SOP(Same-Origin Policy, 동일 출처 정책)'**를 알아야 합니다.

SOP는 아주 간단한 규칙입니다. "내 집(출처)에서만 자원을 가져올 수 있다."

예를 들어, https://my-awesome-site.com에서 실행된 스크립트는 원칙적으로 https://my-awesome-site.com의 리소스에만 접근할 수 있습니다. 만약 이 스크립트가 https://api.secret-bank.com의 데이터를 마음대로 가져올 수 있다면 큰일 나겠죠? SOP는 이런 위험을 막는 기본적인 방화벽입니다.

하지만 현대 웹 서비스는 API 서버, 이미지 서버 등 다양한 출처의 자원을 가져와야만 합니다. 그래서 SOP라는 엄격한 규칙에 '공식적인 허가 절차'를 만들어 준 것이 바로 CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)입니다.

 


초고속 통과! - Simple Request (단순 요청)

 

CORS 요청에는 두 가지 종류가 있습니다. 첫 번째는 공항의 '국내선 승객'처럼 아주 간단한 절차만 거치는 Simple Request입니다.
브라우저는 특정 조건을 모두 만족하는 "안전한" 요청이라고 판단되면, 별도의 허락 절차 없이 요청을 서버로 바로 보냅니다.

✅ Simple Request가 되기 위한 체크리스트 (모두 만족해야 함)

메서드: GET, HEAD, POST 중 하나인가?
헤더: 직접 추가한 헤더가 Accept, Accept-Language, Content-Language 등 극히 일부의 기본 헤더뿐인가? (가장 중요한 Authorization 같은 인증 헤더가 없어야 합니다.)
Content-Type: POST 요청의 경우, Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나인가?

 

🚨 여기서 함정!
대부분의 최신 API는 Content-Type: application/json을 사용하고, Authorization 헤더로 인증을 처리합니다. 따라서 우리가 개발하며 마주치는 대부분의 API 요청은 Simple Request 조건을 만족하지 못합니다.

 


특별 검문이 필요합니다 - Preflight Request (예비 요청)

 

Simple Request의 까다로운 조건을 하나라도 어기면, 브라우저는 이 요청을 "잠재적으로 위험할 수 있다"고 판단합니다. 마치 공항에서 '특별 허가가 필요한 물품'을 소지한 승객처럼 말이죠.

이때 브라우저는 본 요청을 보내기 전에, OPTIONS 라는 메서드를 사용해 서버에 "이런 요청을 보내도 될까요?"라고 물어보는 예비 요청을 먼저 보냅니다. 이것이 바로 Preflight Request입니다.

 


✈️ Preflight 요청의 2단계 동작 방식

1단계: 예비 질문 (OPTIONS 요청)

브라우저는 실제 요청을 잠시 멈추고, OPTIONS 요청을 서버에 보냅니다. 이 질문지에는 다음과 같은 내용이 담겨 있습니다.

Access-Control-Request-Method: "제가 곧 PUT 메서드로 요청을 보낼 건데요,"
Access-Control-Request-Headers: "Authorization 헤더랑 Content-Type: application/json 헤더도 같이 보낼 건데, 괜찮을까요?"

 

2단계: 서버의 허가 응답

서버는 이 OPTIONS 요청을 받고, 자신의 CORS 정책을 확인한 뒤 응답 헤더에 '허가증'을 담아 보내줍니다.

Access-Control-Allow-Origin: "네, https://my-awesome-site.com 출처는 허용합니다."
Access-Control-Allow-Methods: "우리 서버는 GET, POST, PUT, DELETE 메서드를 허용해요."
Access-Control-Allow-Headers: "Authorization과 Content-Type 헤더 모두 괜찮아요."

 

3단계: 본 요청 전송

브라우저는 서버가 보내준 '허가증'을 확인합니다. 자신이 보내려던 실제 요청이 허용 범위 안에 있다면, 비로소 멈춰두었던 실제 PUT 요청을 서버로 전송합니다. 만약 허가증 내용이 일치하지 않는다면? 그 자리에서 요청을 차단하고 우리에게 익숙한 CORS 에러를 보여주는 것이죠.

 


마치며: 그래서 개발자는 뭘 해야 할까?

 

CORS 에러를 만났을 때, 이제 우리는 무작정 좌절할 필요가 없습니다.

프론트엔드 개발자라면?
브라우저 개발자 도구의 'Network' 탭을 열어보세요. OPTIONS 요청이 있는지, 있다면 서버가 응답 헤더(Access-Control-Allow-*)를 제대로 보내줬는지 확인하는 것이 첫 번째 단계입니다. 에러의 원인은 대부분 서버 측 설정에 있습니다.

백엔드 개발자라면?
우리가 바로 '허가증'을 발급해주는 주체입니다. 서버의 API가 어떤 출처, 어떤 메서드, 어떤 헤더를 허용할지 명확하게 설정하고, 요청에 맞는 CORS 응답 헤더를 내려주어야 합니다. 대부분의 웹 프레임워크는 이를 쉽게 처리할 수 있는 미들웨어나 플러그인을 제공합니다.

 


CORS는 우리를 괴롭히는 버그가 아니라, 웹 생태계를 안전하게 지키는 규칙입니다. 이 규칙의 원리를 이해하면, 우리는 더 견고하고 안전한 웹 서비스를 만드는 유능한 개발자로 거듭날 수 있을 것입니다.