CORS 오류가 나는 진짜 이유와 해결법

2026. 10. 17. 13:00·IT
728x90
반응형

프런트엔드에서 API를 호출했는데 콘솔에 CORS 오류가 뜬다. 서버 개발자에게 물어보면 "서버에서는 잘 되는데요"라고 한다. 둘 다 맞는 말이다. CORS는 브라우저가 거는 제약이지 서버의 문제가 아니기 때문이다. 이 글은 CORS가 왜 존재하고 오류가 정확히 무엇을 뜻하는지, 그리고 상황별로 어떻게 푸는지를 정리한다.

브라우저만 막는다

먼저 이 사실부터 분명히 해야 한다. curl이나 서버 코드에서 같은 API를 호출하면 아무 문제 없이 된다. CORS는 브라우저가 자바스크립트로 다른 출처에 요청할 때 적용하는 규칙이다.

왜 이런 제약이 있나. 브라우저는 쿠키를 자동으로 붙인다. 만약 제약이 없다면, 악성 사이트가 사용자가 로그인해 둔 은행 사이트에 요청을 보내고 응답을 읽을 수 있다. 쿠키가 자동으로 붙으니 인증도 통과한다. 이걸 막는 것이 동일 출처 정책(Same-Origin Policy)이고, CORS는 그 제약을 서버가 의도적으로 풀어주는 방법이다.

출처(origin)는 스킴 + 호스트 + 포트로 정의된다. 하나라도 다르면 다른 출처다.

https://app.example.com        기준
http://app.example.com         다름 (스킴)
https://api.example.com        다름 (호스트)
https://app.example.com:8443   다름 (포트)

서브도메인이 달라도 다른 출처라는 점이 자주 헷갈린다.

프리플라이트가 붙는 조건

CORS 요청은 두 종류로 나뉜다. 단순 요청은 바로 나가고, 그렇지 않으면 브라우저가 먼저 OPTIONS 요청을 보내 허락을 구한다. 이걸 프리플라이트라고 한다.

단순 요청의 조건은 대략 이렇다. 메서드가 GET, HEAD, POST 중 하나이고, 헤더가 몇 가지 기본 항목뿐이며, Content-Type이 text/plain, multipart/form-data, application/x-www-form-urlencoded 중 하나여야 한다.

Content-Type: application/json은 단순 요청이 아니다. 그래서 요즘 대부분의 API 호출은 프리플라이트가 붙는다. Authorization 헤더를 붙여도 마찬가지다.

서버가 응답해야 하는 헤더

서버 쪽에서 필요한 것은 몇 개 되지 않는다.

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Max-Age는 프리플라이트 결과를 브라우저가 캐시할 시간이다. 이걸 안 주면 요청마다 OPTIONS가 한 번씩 더 나간다. 왕복이 두 배가 되므로 반드시 설정하는 편이 좋다.

쿠키나 인증 정보를 함께 보내려면 양쪽에 설정이 필요하다.

fetch(url, { credentials: "include" })
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

여기서 중요한 제약이 있다. credentials를 쓰면 Allow-Origin에 *를 쓸 수 없다. 정확한 출처를 적어야 한다. 이건 사양상 금지이고, 브라우저가 거부한다.

자주 겪는 함정

와일드카드를 남발하는 것. Access-Control-Allow-Origin: *는 편하지만, 인증이 필요한 API에 쓰면 안 된다. 허용할 출처 목록을 두고 요청의 Origin과 대조해 그 값을 그대로 돌려주는 방식이 맞다. 이때 Vary: Origin 헤더를 함께 줘야 캐시가 출처별로 분리된다.

에러 응답에 CORS 헤더가 빠지는 것. 서버가 500을 낼 때 CORS 헤더를 안 붙이면, 브라우저는 CORS 오류로 표시한다. 개발자는 CORS 설정을 뒤지지만 실제 원인은 서버 에러다. 예외 처리 경로에도 CORS 헤더가 붙는지 확인해야 한다.

리다이렉트. 프리플라이트 응답이 리다이렉트되면 실패한다. OPTIONS 요청이 인증 미들웨어에 걸려 로그인 페이지로 넘어가는 경우가 흔하다. OPTIONS는 인증에서 제외해야 한다.

프록시가 헤더를 지우는 것. nginx나 로드 밸런서가 중간에서 헤더를 제거하거나 덮어쓰는 경우가 있다. 애플리케이션과 프록시 양쪽에서 CORS 헤더를 설정하면 중복되어 오히려 깨진다. 한 곳에서만 설정한다.

개발 환경에서는 프록시가 편하다

로컬 개발에서 프런트엔드가 localhost:3000, API가 localhost:8080이면 출처가 달라 CORS가 걸린다. 서버에 개발용 CORS 설정을 추가하는 방법도 있지만, 개발 서버의 프록시 기능을 쓰는 편이 깔끔하다.

// vite.config.js
export default {
  server: {
    proxy: { "/api": { target: "http://localhost:8080", changeOrigin: true } },
  },
};

이러면 브라우저 입장에서는 같은 출처로 요청하는 것이 되어 CORS가 아예 발생하지 않는다. 프로덕션에서도 같은 도메인 아래 경로로 API를 두는 구성이라면 동일하게 문제가 없다.

보안 장치이지 장애물이 아니다

CORS 오류를 만나면 일단 *로 열어서 넘어가고 싶어진다. 그 마음은 이해하지만, CORS는 나를 막으려는 것이 아니라 사용자를 지키려는 것이라는 점을 기억하면 접근이 달라진다.

허용 출처를 정확히 적는 데 드는 비용은 크지 않다. 환경별로 목록을 관리하고, 운영 환경에는 운영 도메인만 넣으면 된다. 반대로 전부 열어두면, 어느 사이트에서든 사용자의 쿠키를 달고 내 API를 호출할 수 있게 된다.

한 가지 덧붙이면, CORS는 응답을 읽는 것을 막지 요청이 서버에 도달하는 것을 막지는 않는다. 단순 요청이라면 서버는 이미 처리한 뒤다. 그래서 상태를 바꾸는 작업을 보호하려면 CORS만으로는 부족하고 CSRF 토큰이나 SameSite 쿠키 속성이 함께 필요하다. 이 둘을 같은 것으로 오해하는 경우가 많은데, 막는 지점이 다르다.

참고 자료

  • Cross-Origin Resource Sharing (CORS) — MDN — 프리플라이트 조건, 헤더 전체 목록, 자격 증명 규칙
  • Same-origin policy — MDN — 출처의 정의와 정책의 배경
  • OWASP: Cross-Site Request Forgery Prevention Cheat Sheet — CORS 와 CSRF 의 역할 구분
728x90
반응형

'IT' 카테고리의 다른 글

헬스체크와 무중단 배포, 프로브 제대로 쓰기  (0) 2026.10.19
페이지네이션, 오프셋 대신 커서를 쓸 때  (0) 2026.10.18
JWT와 세션, 무엇을 골라야 하나  (0) 2026.10.16
구조화 로깅, 장애 때 찾을 수 있는 로그 남기기  (0) 2026.10.15
캐시 전략, 무효화와 스탬피드 대응  (0) 2026.10.14
'IT' 카테고리의 다른 글
  • 헬스체크와 무중단 배포, 프로브 제대로 쓰기
  • 페이지네이션, 오프셋 대신 커서를 쓸 때
  • JWT와 세션, 무엇을 골라야 하나
  • 구조화 로깅, 장애 때 찾을 수 있는 로그 남기기
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (463)
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (66)
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    svelte
    rust linux
    docker
    에디터 팁
    proxy pass
    vue cli
    css perspective
    자바 생성자
    제미나이
    css tb
    자바 클래스
    티스토리 자동화
    jsp parameter
    ssl 인증서 발급
    Java Array
    리눅스 설치
    nginx ssl 적용
    servlet class
    css table
    spring java
    css transition
    vscode
    러스트
    오류
    nginx ssl 설정
    SSL 인증서
    nginx
    자바 배열
    브라우저 자동화
    vue 설치
    Extension Bisect
    css float
    VS Code 팁
    React Compiler
    Node
    AI 코딩 에이전트
    프런트엔드
    AI 반도체
    API 설계
    클론코딩
    자바 객체 지향
    ubuntu
    Rust lang
    gitlab 설치
    자바 exception
    자바 for문
    java casting
    jsp include
    mysql db
    lang rust
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
CORS 오류가 나는 진짜 이유와 해결법
상단으로

티스토리툴바