본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 22. 09:56

개발자들이 CORS를 이해하지 못하는 이유 (그리고 해결 방법)

요약

웹 개발에서 빈번하게 발생하는 CORS(Cross-Origin Resource Sharing)의 개념적 오해와 올바른 해결 방법을 다룹니다. CORS가 서버 보안이 아닌 브라우저 보안 메커니즘임을 강조하며, 잘못된 우회 방식 대신 근본적인 이해를 바탕으로 한 구현을 권장합니다.

핵심 포인트

  • CORS는 서버 보호가 아닌 브라우저의 사용자 보호를 위한 메커니즘임
  • 동일 출처 정책(SOP)을 통제된 방식으로 완화하는 수단임
  • 브라우저 확장 프로그램이나 프록시를 통한 우회는 근본적인 해결책이 아님
  • Access-Control-Allow-Origin: * 와 같은 무분별한 설정 지양 필요

개발자들이 CORS를 이해하지 못하는 이유 (그리고 해결 방법)

Meta Description: 개발자들이 CORS를 이해하지 못한다(2019)는 내용은 오늘날에도 여전히 유효합니다. 왜 Cross-Origin Resource Sharing (CORS)가 개발자들을 혼란스럽게 만드는지, 그리고 2026년에 이를 어떻게 올바르게 구현할 수 있는지 알아보세요.

TL;DR: CORS (Cross-Origin Resource Sharing)는 웹 개발에서 가장 오해받는 브라우저 보안 메커니즘 중 하나입니다. 개발자들의 광범위한 혼란을 강조했던 2019년의 기념비적인 논의에도 불구하고, 동일한 실수들이 오늘날에도 지속되고 있습니다. 이 글은 왜 CORS가 개발자들을 넘어뜨리는지, 브라우저가 실제로 무엇을 하고 있는지, 그리고 모든 것에 단순히 Access-Control-Allow-Origin: *를 붙여버리고 끝내는 것이 아니라 어떻게 올바르게 구성할 수 있는지 정확히 분석합니다.

2019년의 경종: 개발자들은 CORS를 이해하지 못한다

지난 2019년, 널리 공유된 블로그 포스트와 그에 이은 Hacker News 스레드는 시니어 개발자들이 수년간 조용히 관찰해 온 사실을 구체화했습니다: 대부분의 개발자가 CORS를 근본적으로 오해하고 있다는 점입니다. 이 포스트가 공감을 얻은 이유는 이것이 단순한 초보자의 실수가 아니라, 숙련된 엔지니어들에게도 영향을 미치는 개념적 격차라는 실제적이고 지속적인 문제를 지적했기 때문입니다.

7년이 지난 지금도 상황은 극적으로 개선되지 않았습니다. Stack Overflow에는 여전히 매달 수천 개의 CORS 관련 질문이 올라옵니다. "CORS error"는 웹 개발에서 가장 많이 구글링되는 에러 메시지 중 하나로 남아 있습니다. 그리고 결정적으로, CORS를 완전히 비활성화하거나, 브라우저 확장 프로그램을 사용하여 우회하거나, 허용적인 헤더를 맹목적으로 복사하여 붙여넣는 것과 같은 잘못된 해결책들이 여전히 가장 흔한 대응 방식입니다.

그렇다면 실제로 무슨 일이 일어나고 있는 걸까요? 기초부터 제대로 이해해 봅시다.

[INTERNAL_LINK: browser security fundamentals]

CORS란 실제로 무엇인가 (그리고 무엇이 아닌가)

오해를 진단하기 전에, 올바른 멘탈 모델 (Mental Model)을 확립해야 합니다.

CORS는 서버 기능이 아니라 브라우저 기능이다

이것이 이해해야 할 가장 중요한 사항이며, 대부분의 혼란이 시작되는 지점입니다.

CORS는 서버를 보호하지 않습니다. 그것은 사용자를 보호합니다.

브라우저가 교차 출처 요청(cross-origin request)을 보낼 때 — 예를 들어, app.example.com의 JavaScript가 api.otherdomain.com의 API를 호출할 때 — 브라우저는 **동일 출처 정책 (Same-Origin Policy, SOP)**이라 불리는 정책을 강제합니다. 이 정책은 악의적인 웹사이트가 사용자를 대신하여 다른 사이트에 인증된 요청을 보내는 것을 방지하기 위해 존재합니다.

CORS는 서버가 해당 정책을 통제된 방식으로 *완화 (relax)*할 수 있게 해주는 메커니즘입니다. 서버는 브라우저에게 다음과 같이 말합니다: "네, 저는 app.example.com으로부터의 요청을 받는 것에 동의합니다." 그러면 브라우저는 응답이 통과되도록 허용합니다.

핵심 통찰: 만약 브라우저 확장 프로그램이나 프록시(proxy)를 사용하여 CORS 체크를 비활성화한다면, 당신은 무언가를 "고치고" 있는 것이 아닙니다. 당신은 사용자 보호 메커니즘을 우회하고 있는 것입니다. 당신의 API는 여전히 똑같이 접근 가능한 상태이며, 단지 사용자를 위한 안전망을 제거했을 뿐입니다.

동일 출처 정책 (Same-Origin Policy): 빠른 복습

두 URL이 다음 세 가지 항목 모두에서 일치하면 동일한 출처(origin)를 공유합니다:

  • 프로토콜 (Protocol) (http vs https)
  • 도메인 (Domain) (example.com vs otherdomain.com)
  • 포트 (Port) (3000 vs 8080)
URL 비교동일 출처 여부이유
https://example.com vs https://example.com/api✅ 예동일한 프로토콜, 도메인, 포트
...

개발자들이 CORS를 잘못 이해하는 이유: 근본 원인

2019년의 논의에서는 몇 가지 반복되는 오해의 패턴을 확인했습니다. 이를 구조적으로 분석하면 다음과 같습니다.

1. CORS를 서버 측 보안 메커니즘으로 취급함

많은 개발자가 자신의 API를 보호하고 있다고 생각하며 CORS 헤더를 추가합니다. 하지만 그렇지 않습니다 — 적어도 그들이 생각하는 방식으로는 말입니다. curl 요청, Postman 호출, 또는 서버 간 요청(server-to-server request)은 CORS를 완전히 우회합니다. 브라우저만이 CORS를 강제하며, 그 외의 어떤 것도 강제하지 않습니다.

실질적인 결과: API를 보호하기 위해 CORS에 의존하는 것은 치명적인 실수입니다. 여전히 적절한 인증 (authentication), 인가 (authorization), 속도 제한 (rate limiting), 그리고 입력값 검증 (input validation)이 필요합니다. CORS는 이 중 그 어떤 것의 대체제도 아닙니다.

2. 단순 요청 (Simple Requests)과 프리플라이트 요청 (Preflight Requests)의 혼동

이 지점에서 숙련된 개발자들조차 어려움을 겪습니다. CORS에는 두 가지 명확히 구분되는 흐름(flow)이 있습니다:

단순 요청 (Simple Requests)

요청이 '단순하다'는 것은 다음 모든 기준을 충족한다는 것을 의미합니다:

  • 메서드(Method)가 GET, POST, 또는 HEAD인 경우
  • Content-Type이 application/x-www-form-urlencoded, multipart/form-data, 또는 text/plain인 경우
  • 사용자 정의 헤더(custom headers)가 없는 경우

단순 요청은 직접 전송되며, 브라우저가 나중에 응답 헤더를 확인합니다.

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

'단순' 기준을 충족하지 못하는 모든 요청은 **프리플라이트(preflight)**를 유발합니다. 이는 실제 요청이 발생하기 전에 브라우저가 보내는 자동 OPTIONS 요청입니다. 브라우저는 서버에게

"'https://app.example.com' 오리진(origin)에서 'https://api.example.com'으로의 fetch에 대한 접근이 CORS 정책에 의해 차단되었습니다: 요청된 리소스에 'Access-Control-Allow-Origin' 헤더가 존재하지 않습니다.""

개발자들은 종종 이를 "서버가 내 요청을 거부했다"라고 잘못 읽곤 합니다. 실제로는 서버가 완벽하게 유효한 200 응답을 반환했을 수도 있습니다. 다만 응답에 적절한 CORS 헤더가 누락되었기 때문에 브라우저가 JavaScript가 해당 응답을 읽는 것을 차단하고 있는 것입니다.

이 차이점은 디버깅(debugging) 시 매우 중요합니다.

[INTERNAL_LINK: browser developer tools guide]

CORS를 올바르게 구현하는 방법: 실무 가이드

실제로 알아야 할 헤더들

헤더방향목적
Access-Control-Allow-Origin응답 (Response)허용된 오리진 (origins)
...

프레임워크별 구현 방법

Node.js / Express

cors npm 패키지가 표준 솔루션이며 대부분의 유스케이스(use cases)를 잘 처리합니다. 활발하게 유지 관리되고 있으며 문서화도 잘 되어 있습니다.

const cors = require('cors');

const corsOptions = {
...

솔직한 평가: cors 패키지는 대부분의 유스케이스에 탁월하지만, 동적 오리진 검증(함수 전달)을 정확하게 구현하는 것은 까다로울 수 있습니다. 허용된 오리진과 허용되지 않은 오리진 모두를 사용하여 철저히 테스트하세요.

Python / Django

django-cors-headers는 Django 프로젝트를 위한 필수 솔루션입니다.

# settings.py
INSTALLED_APPS = [
    ...
...

Nginx 설정

리버스 프록시(reverse proxy) 레벨에서 CORS를 처리하는 경우:

location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
...

흔한 CORS 실수와 해결 방법

실수 #1: OPTIONS 요청을 처리하지 않음

서버는 OPTIONS 요청 (preflight)에 대해 200 또는 204 상태 코드와 적절한 헤더를 포함하여 응답해야 합니다. 많은 프레임워크가 이를 자동으로 처리하지 않습니다.

해결 방법: 라우팅에서 OPTIONS를 명시적으로 처리하거나, 이를 대신 처리해 주는 미들웨어 (middleware)를 사용하세요.

실수 #2: 성공 응답에만 CORS 헤더를 추가하는 경우

API가 401 또는 500 에러를 반환하는 경우에도 CORS 헤더가 필요합니다. 그렇지 않으면 브라우저가 에러 응답을 차단하며, 프론트엔드(frontend)는 실제 에러 메시지 대신 일반적인 CORS 에러를 받게 됩니다.

해결 방법: 에러를 포함한 모든 응답에 CORS 헤더를 추가하세요. 개별 라우트 핸들러 (route handler)가 아닌 미들웨어 레벨에서 이를 설정해야 합니다.

실수 #3: 동적 Origin 사용 시의 캐싱 문제

여러 Origin을 지원하고 요청의 Origin 헤더를 기반으로 Access-Control-Allow-Origin을 동적으로 설정하는 경우, 반드시 다음을 설정해야 합니다:

Vary: Origin

이 설정이 없으면 CDN이나 브라우저 캐시가 다른 요청자에게 잘못된 Origin 헤더가 포함된 응답을 제공할 수 있습니다.

실수 #4: Fetch 호출 시 Credentials를 누락하는 경우

서버가 Access-Control-Allow-Credentials: true를 올바르게 설정하더라도, 프론트엔드에서 명시적으로 옵트인 (opt in) 해야 합니다:

fetch('https://api.example.com/data', {
  credentials: 'include'  // 이것을 잊지 마세요!
});

CORS 디버깅을 위한 유용한 도구들

브라우저 개발자 도구 (Browser DevTools)

가장 먼저 확인해야 할 곳입니다. 네트워크 (Network) 탭은 preflight OPTIONS 호출을 포함한 모든 요청을 보여줍니다. 콘솔 (Console) 탭은 구체적인 CORS 에러를 보여줍니다. Chrome과 Firefox 모두 2026년 기준으로 상당히 상세한 CORS 에러 메시지를 제공합니다.

Hoppscotch

무료 오픈 소스 API 테스트 도구입니다 (Postman의 대안). 브라우저의 CORS 제한 없이 API 엔드포인트 (endpoint)를 직접 테스트할 수 있어, 문제가 서버 설정 때문인지 브라우저 동작 때문인지 격리하여 파악하는 데 유용합니다.

솔직한 의견: 빠른 테스트에는 훌륭하지만, 복잡한 워크플로를 위한 기능 집합은 여전히 Postman이 더 성숙해 있습니다.

CORS Tester by Requestly

Requestly는 요청/응답(requests/responses)을 가로채고 수정할 수 있는 브라우저 확장 프로그램을 제공하며, 이는 개발 과정에서 CORS 문제를 디버깅할 때 매우 유용합니다. 서버 코드를 변경하지 않고도 헤더를 추가하거나 수정할 수 있습니다.

솔직한 의견: 로컬 개발 디버깅에 매우 유용합니다. 프로덕션 환경에서 영구적인 해결책으로 사용하지 마세요. 서버에서 근본 원인을 해결해야 합니다.

기준 테스트를 위한 curl

# 프리플라이트(preflight) 요청을 수동으로 테스트
curl -X OPTIONS https://api.example.com/data \
  -H "Origin: https://app.example.com" \
...

만약 서버가 curl에 올바른 헤더를 반환하지 않는다면, 브라우저에서도 확실히 작동하지 않을 것입니다.

[INTERNAL_LINK: API 디버깅 기술]

핵심 요약 (Key Takeaways)

  • CORS는 서버가 아닌 사용자를 보호합니다. 이는 브라우저의 강제 메커니즘입니다. 서버 간 요청(Server-to-server requests)이나 curl과 같은 도구는 이를 완전히 무시합니다.
  • 동일 출처 정책 (Same-Origin Policy)이 기본입니다. CORS는 이 정책을 선택적으로 완화하는 방법입니다.
  • 프리플라이트 (Preflight) 요청은 자동입니다. 단순하지 않은(non-simple) 모든 요청은 OPTIONS 프리플라이트를 트리거합니다. 서버는 이를 처리해야 합니다.
  • 와일드카드와 자격 증명(credentials)은 함께 사용할 수 없습니다. Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true와 함께 사용할 수 없습니다.
  • 에러를 포함한 모든 응답에 CORS 헤더를 추가하세요. 개별 라우트(per-route) 설정보다 미들웨어(Middleware) 수준의 설정이 더 안전합니다.
  • 요청의 Origin을 동적으로 반영할 때는 항상 Vary: Origin을 설정하세요.
  • CORS는 보안 도구가 아닙니다. 적절한 인증(authentication) 및 인가(authorization)는 별도로 구현하세요.

마치며 및 행동 유도 (Final Thoughts and CTA)

개발자들의 CORS 혼란에 관한 2019년의 논의가 가치 있었던 이유는, 그것이 실제적이고 구조적인 문제를 지적했기 때문입니다. 이는 대부분의 개발자가 명시적으로 배운 적 없는 보안 모델을 브라우저가 다소 불투명하게 강제하는 데서 기인합니다. 다행인 점은 올바른 사고 모델(서버 보안이 아닌 브라우저 기능)을 내재화하고 나면, 나머지 모든 것들이 자연스럽게 이해된다는 것입니다.

**실행 항목 (Your action items):

  1. 현재의 CORS 설정을 감사(Audit)하세요 — 사용하지 말아야 할 곳에 *를 사용하고 있지는 않습니까?
  2. 서버가 OPTIONS 요청을 올바르게 처리하는지 확인하세요.
  3. 허용된 오리진(Origin)을 동적으로 설정하는 경우 Vary: Origin을 추가하세요.
  4. CORS 헤더가 성공적인 응답뿐만 아니라 모든 응답에 나타나는지 확인하세요.

이 내용이 도움이 되었다면, [INTERNAL_LINK: 웹 보안 모범 사례] 및 [INTERNAL_LINK: REST API 설계 원칙]에 관한 관련 가이드를 확인해 보세요. 그리고 새로운 프로젝트를 설정 중이라면, 나중에 사후 수정(Retrofitting)하기보다는 첫날부터 잘 구성된 CORS 미들웨어(Middleware)로 시작하는 것을 고려하십시오.

자주 묻는 질문 (Frequently Asked Questions)

Q1: 왜 제 CORS 요청은 Postman에서는 작동하는데 브라우저에서는 실패하나요?

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0