브라우저에서만 발생하는 CORS 오류 ‘No Access-Control-Allow-Origin Header’ — Postman은 왜 정상일까?

1. 문제 정의

프론트엔드 JavaScript 코드에서 다른 도메인의 서버로 요청을 보낼 때, 다음과 같은 오류가 브라우저에서만 발생하는 경우가 많습니다.

1
No 'Access-Control-Allow-Origin' header is present on the requested resource.

대표적인 발생 상황은 다음과 같습니다.

  • 페이지가 호스팅된 도메인(예: 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를 통과하려면 서버 응답에 다음 헤더가 필요합니다.

1
Access-Control-Allow-Origin: https://myfrontend.com

특정 출처 대신 모든 출처를 허용하려면 와일드카드를 사용합니다.

1
Access-Control-Allow-Origin: *

참고: *는 자격 증명(credentials)이 포함된 요청에는 사용할 수 없으며, 그 경우 명시적 출처를 지정해야 합니다.

프레임워크별 서버 설정 예시

Express(Node.js)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const express = require('express');
const app = express();

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://myfrontend.com');
  next();
});

app.get('/api/data', (req, res) => {
  res.json({ message: 'ok' });
});

전용 미들웨어인 cors 패키지를 사용할 수도 있습니다.

1
2
const cors = require('cors');
app.use(cors()); // 모든 출처 허용

Python Flask

1
2
3
4
5
6
7
8
9
from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # 모든 출처 허용

@app.route('/api/data')
def data():
    return jsonify(message='ok')

Django

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# settings.py
INSTALLED_APPS = [
    # ...
    'corsheaders',
]

MIDDLEWARE = [
    # ...
    'corsheaders.middleware.CorsMiddleware',
]

CORS_ALLOWED_ORIGINS = [
    'https://myfrontend.com',
]

사전 요청(Preflight) 주의사항

실제 요청이 application/json Content-Type이나 커스텀 헤더를 사용하는 등 ‘단순 요청(Simple Request)’ 조건을 벗어나면, 브라우저는 본 요청 전에 **OPTIONS 메서드의 사전 요청(Preflight)**을 먼저 보냅니다. 서버가 이에 응답하지 않으면 동일하게 CORS 오류가 발생합니다.

사전 요청 응답에 필요한 헤더 예시:

1
2
3
Access-Control-Allow-Origin: https://myfrontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

디버깅 체크리스트

체크 항목확인 방법
응답에 Access-Control-Allow-Origin 헤더가 있는가개발자 도구 > Network 탭 > 응답 헤더 확인
현재 페이지 출처가 허용 목록에 포함되는가요청 출처와 헤더 값 비교
Preflight(OPTIONS)가 정상 응답하는가Network 탭에서 OPTIONS 요청 확인
자격 증명 요청인가credentials: 'include' 사용 시 * 불가, 명시적 출처 필요

5. 향후 예방 조치

같은 문제를 반복하지 않으려면 다음을 기억하세요.

  1. “Postman은 정상인데 브라우저만 오류"는 곧 CORS 문제다. 서버가 요청을 거부한 것이 아니라 브라우저가 출처 검증 후 응답을 차단한 것이다. 먼저 서버가 CORS 헤더를 보내는지 확인하세요.
  2. 셋업 시점에 CORS를 고려하라. 프론트엔드와 백엔드 도메인이 다른 구조라면 서버 코드를 작성할 때 처음부터 Access-Control-Allow-Origin 처리를 포함하세요. 보통 프레임워크의 CORS 미들웨어를 활성화하는 것만으로 해결됩니다.
  3. 환경별 출처를 명시적으로 관리하라. 개발/스테이징/운영 환경마다 허용할 프론트엔드 출처가 다릅니다. 환경 변수로 출처 목록을 관리하면 잘못된 설정을 줄일 수 있습니다.
  4. 모든 출처 허용(*)은 개발용으로만 사용하라. 운영 환경에서 모든 출처를 허용하면 보안 위험이 있습니다. 반드시 신뢰할 수 있는 출처만 명시하세요.
  5. 자격 증명을 쓰는 요청은 명시적 출처를 사용하라. 인증 쿠키 등 credentials를 포함한 요청에는 와일드카드가 동작하지 않으며, 서버에서 정확한 출처를 명시해야 합니다.
  6. 오류 메시지를 그대로 검색하지 말고 “왜 발생하는지"를 먼저 이해하라. 선언한 프레임워크가 다르면 설정 방법이 달라지지만, 근본 원리(브라우저의 Same-Origin Policy + 서버의 CORS 헤더)는 동일합니다.

참고 출처