브라우저에서만 발생하는 CORS 오류 ‘No Access-Control-Allow-Origin Header’ — Postman은 왜 정상일까?
1. 문제 정의
프론트엔드 JavaScript 코드에서 다른 도메인의 서버로 요청을 보낼 때, 다음과 같은 오류가 브라우저에서만 발생하는 경우가 많습니다.
| |
대표적인 발생 상황은 다음과 같습니다.
- 페이지가 호스팅된 도메인(예:
https://myfrontend.com)과 다른 도메인(예:https://api.example.com)으로XMLHttpRequest또는fetch를 보낼 때 - 동일한 요청을 Postman, curl 등 클라이언트 도구로 보내면 아무 문제 없이 정상 응답을 받는데, 브라우저에서만 위 오류가 발생
이때 핵심 의문은 “요청과 서버는 같은데 왜 브라우저에서만 오류가 나는가?“입니다. 이는 오류 자체의 해결 방법보다 왜 이런 차이가 발생하는지를 이해하는 것이 더 중요합니다.
2. 원인 탐구
첫 번째 단서는 “어디서 오류가 발생하는가"입니다.
- Postman은 정상 응답을 받는다.
- 브라우저에서만 오류가 발생한다.
서버가 요청을 거부해서 오류가 났다면, Postman과 curl에서도 동일하게 실패했을 것입니다. 그런데 클라이언트 도구에서는 성공하고 브라우저에서만 실패한다는 것은, 서버가 요청을 처리했지만 브라우저가 응답을 차단했음을 의미합니다.
두 번째 단서는 브라우저가 무언가 다른 보안 규칙을 적용한다는 점입니다. 이 규칙이 바로 Same-Origin Policy(동일 출처 정책) 입니다.
정리하면 오류의 원인 후보는 다음과 같습니다.
| 가정 | 검증 |
|---|---|
| 서버가 요청을 거부함 | ❌ Postman과 curl은 정상 응답을 받음 |
| 네트워크/인증 문제 | ❌ 동일 요청이 클라이언트 도구에서는 성공 |
| 브라우저의 Same-Origin Policy가 응답을 차단 | ✅ 브라우저에서만 발생함 |
따라서 오류는 서버가 아니라 브라우저의 보안 정책 단계에서 발생합니다.
3. 근본 원인 분석
근본 원인은 브라우저와 API 클라이언트의 본질적인 차이입니다.
- 브라우저는 웹 페이지가 다른 출처의 리소스를 읽는 것을 막는 Same-Origin Policy를 적용합니다. 정규 웹 페이지는
XMLHttpRequest객체로 원격 서버와 데이터를 주고받을 수 있지만, 동일 출처(Same Origin) 제한을 받습니다. 즉, 현재 페이지와 다른 출처(Cross-Origin) 로의 요청은 기본적으로 차단됩니다. - Postman은 브라우저가 아닙니다. 요청을 보내는 주체가 브라우저가 아니므로 Same-Origin Policy의 적용을 받지 않습니다. 따라서 교차 출처 요청이든 무엇이든 서버가 응답하는 대로 그대로 받아서 보여줍니다.
이 때문에 “같은 요청인데 브라우저에서만 오류, Postman은 정상"이라는 현상이 발생합니다. 브라우저는 교차 출처 응답에 접근하기 전에 서버가 “이 출처를 허용한다"는 신호를 보냈는지 확인하며, 그 신호가 바로 CORS(Cross-Origin Resource Sharing) 헤더입니다.
응답에 Access-Control-Allow-Origin 헤더가 없으면 브라우저는 해당 응답을 차단하고 위 오류를 던집니다. 반대로 Postman은 이런 출처 검증을 하지 않으므로 오류가 발생하지 않습니다.
4. 코드 해결책
핵심은 서버가 응답에 CORS 헤더를 포함하도록 만드는 것입니다. 프론트엔드 JavaScript만으로는 이 오류를 우회할 수 없습니다(보안상 브라우저는 이를 허용하지 않습니다).
교차 출처 요청이 CORS를 통과하려면 서버 응답에 다음 헤더가 필요합니다.
| |
특정 출처 대신 모든 출처를 허용하려면 와일드카드를 사용합니다.
| |
참고:
*는 자격 증명(credentials)이 포함된 요청에는 사용할 수 없으며, 그 경우 명시적 출처를 지정해야 합니다.
프레임워크별 서버 설정 예시
Express(Node.js)
| |
전용 미들웨어인 cors 패키지를 사용할 수도 있습니다.
| |
Python Flask
| |
Django
| |
사전 요청(Preflight) 주의사항
실제 요청이 application/json Content-Type이나 커스텀 헤더를 사용하는 등 ‘단순 요청(Simple Request)’ 조건을 벗어나면, 브라우저는 본 요청 전에 **OPTIONS 메서드의 사전 요청(Preflight)**을 먼저 보냅니다. 서버가 이에 응답하지 않으면 동일하게 CORS 오류가 발생합니다.
사전 요청 응답에 필요한 헤더 예시:
| |
디버깅 체크리스트
| 체크 항목 | 확인 방법 |
|---|---|
응답에 Access-Control-Allow-Origin 헤더가 있는가 | 개발자 도구 > Network 탭 > 응답 헤더 확인 |
| 현재 페이지 출처가 허용 목록에 포함되는가 | 요청 출처와 헤더 값 비교 |
| Preflight(OPTIONS)가 정상 응답하는가 | Network 탭에서 OPTIONS 요청 확인 |
| 자격 증명 요청인가 | credentials: 'include' 사용 시 * 불가, 명시적 출처 필요 |
5. 향후 예방 조치
같은 문제를 반복하지 않으려면 다음을 기억하세요.
- “Postman은 정상인데 브라우저만 오류"는 곧 CORS 문제다. 서버가 요청을 거부한 것이 아니라 브라우저가 출처 검증 후 응답을 차단한 것이다. 먼저 서버가 CORS 헤더를 보내는지 확인하세요.
- 셋업 시점에 CORS를 고려하라. 프론트엔드와 백엔드 도메인이 다른 구조라면 서버 코드를 작성할 때 처음부터
Access-Control-Allow-Origin처리를 포함하세요. 보통 프레임워크의 CORS 미들웨어를 활성화하는 것만으로 해결됩니다. - 환경별 출처를 명시적으로 관리하라. 개발/스테이징/운영 환경마다 허용할 프론트엔드 출처가 다릅니다. 환경 변수로 출처 목록을 관리하면 잘못된 설정을 줄일 수 있습니다.
- 모든 출처 허용(
*)은 개발용으로만 사용하라. 운영 환경에서 모든 출처를 허용하면 보안 위험이 있습니다. 반드시 신뢰할 수 있는 출처만 명시하세요. - 자격 증명을 쓰는 요청은 명시적 출처를 사용하라. 인증 쿠키 등 credentials를 포함한 요청에는 와일드카드가 동작하지 않으며, 서버에서 정확한 출처를 명시해야 합니다.
- 오류 메시지를 그대로 검색하지 말고 “왜 발생하는지"를 먼저 이해하라. 선언한 프레임워크가 다르면 설정 방법이 달라지지만, 근본 원리(브라우저의 Same-Origin Policy + 서버의 CORS 헤더)는 동일합니다.