웹개발 CORS 오류 해결하는 법 실전 가이드
CORS 오류가 갑자기 터지는 진짜 이유
브라우저가 막는 것이지 서버가 무조건 죽은 것은 아닙니다
프론트엔드 화면에서는 버튼을 눌렀을 뿐인데 콘솔에 CORS policy, Access-Control-Allow-Origin, preflight request 같은 문장이 길게 뜨면 당황하기 쉽습니다. 하지만 이 오류는 대부분 API 서버가 완전히 고장 났다는 뜻이 아니라, 브라우저의 보안 정책이 현재 요청을 허용하지 않았다는 신호에 가깝습니다.
2026년 기준 웹개발 환경은 로컬 프론트엔드 서버, 백엔드 API 서버, 인증 서버, 클라우드 스토리지, 외부 결제 API처럼 출처가 다른 서비스가 함께 동작하는 구조가 흔합니다. 그래서 CORS는 초보자만 겪는 문제가 아니라 실무 개발자도 배포 직전 자주 마주치는 대표적인 웹개발 오류입니다. 코딩과 프로그래밍의 기본 개념이 헷갈린다면 코딩의 기본 정의를 먼저 확인해 두면 요청과 응답 흐름을 이해하는 데 도움이 됩니다.
- 출처가 다름: 프로토콜, 도메인, 포트 중 하나라도 다르면 브라우저는 다른 출처로 판단합니다.
- 서버 응답 헤더 부족: API 서버가 허용할 출처를 명확히 알려주지 않으면 브라우저가 응답을 차단합니다.
- 인증 정보 포함 요청: 쿠키나 Authorization 헤더가 들어가면 단순 허용보다 더 엄격한 설정이 필요합니다.
- OPTIONS 요청 실패: 실제 요청 전에 보내는 preflight 요청이 404, 405, 500으로 실패하면 본 요청까지 막힙니다.
팁: CORS 오류를 볼 때는 화면의 에러 문구만 보지 말고, 개발자 도구 Network 탭에서 실제 응답 상태 코드와 Response Headers를 함께 확인해야 합니다.
가장 먼저 확인할 세 가지
문제 해결은 순서가 중요합니다. 무작정 프론트엔드 코드에서 fetch 옵션을 바꾸거나, 서버에 모든 출처를 허용하는 와일드카드를 넣으면 당장은 해결된 것처럼 보일 수 있지만 배포 후 인증, 보안, 쿠키 전달에서 더 큰 문제가 생길 수 있습니다.
- 요청 주소가 의도한 API 주소인지 확인합니다. 로컬에서는 localhost:3000과 localhost:8080도 서로 다른 출처입니다.
- 서버 응답에 Access-Control-Allow-Origin 헤더가 있는지 확인합니다.
- 쿠키 인증을 사용한다면 Access-Control-Allow-Credentials와 프론트엔드의 credentials 옵션이 서로 맞는지 확인합니다.
프론트엔드에서 먼저 점검할 요청 코드
fetch와 axios 설정 실수
CORS는 서버 설정 문제가 많은 편이지만, 프론트엔드 요청 코드가 원인을 키우는 경우도 적지 않습니다. 특히 API 주소를 환경 변수로 분리한 프로젝트에서 개발 서버 주소와 운영 서버 주소가 섞이면, 브라우저는 전혀 다른 출처로 요청을 보내고 서버는 허용하지 않은 Origin을 받게 됩니다.
예를 들어 React, Next.js, Vue, SvelteKit 같은 프레임워크에서 API_BASE_URL을 잘못 지정하면 로컬 테스트는 통과했는데 운영 배포에서만 CORS 오류가 납니다. 이때는 코드 한 줄보다 빌드 시점 환경 변수, 브라우저가 실제로 호출한 URL, 프록시 설정을 함께 봐야 합니다.
- fetch 사용 시: 쿠키가 필요한 요청이라면 credentials: 'include'를 지정해야 합니다.
- axios 사용 시: withCredentials: true 설정이 필요한지 인증 방식에 따라 확인합니다.
- Content-Type 확인: application/json 요청은 preflight를 유발할 수 있으므로 OPTIONS 처리가 필요합니다.
- 커스텀 헤더 확인: Authorization, X-API-Key 같은 헤더는 서버의 Allow-Headers 목록에 포함되어야 합니다.
개발 환경 프록시를 안전하게 쓰는 법
로컬 개발 중이라면 프론트엔드 개발 서버의 프록시 기능을 활용할 수 있습니다. Vite의 server.proxy, Next.js의 rewrites, webpack devServer proxy를 쓰면 브라우저 입장에서는 같은 출처로 요청하는 것처럼 보이게 만들 수 있습니다. 다만 이 방식은 개발 편의 기능이지 운영 서버의 CORS 설정을 대체하지 않습니다.
실무에서는 개발 환경과 운영 환경을 분리해서 생각해야 합니다. 로컬에서는 프록시로 빠르게 작업하되, 스테이징이나 운영 배포 전에는 실제 도메인 기준으로 CORS 허용 목록을 검증해야 합니다. 특히 모바일 앱 웹뷰, 관리자 페이지, 고객용 페이지가 서로 다른 도메인을 쓰면 허용 Origin을 명확히 관리해야 합니다.
- 브라우저 Network 탭에서 Request URL과 Origin 헤더를 확인합니다.
- 프론트엔드 환경 변수 파일의 API 주소가 현재 실행 환경과 맞는지 봅니다.
- 프록시를 쓴다면 요청 경로가 중복으로 붙지 않는지 확인합니다. 예: /api/api/users
- 운영 환경에서는 프록시에 의존하지 말고 백엔드 CORS 정책을 별도로 설정합니다.
백엔드 서버에서 고쳐야 할 CORS 헤더
허용 Origin은 넓게가 아니라 정확하게
가장 흔한 임시 처방은 Access-Control-Allow-Origin: *를 넣는 것입니다. 공개 API처럼 인증이 전혀 없고 누구나 조회 가능한 데이터라면 일부 상황에서 사용할 수 있지만, 로그인 쿠키나 세션, 사용자 개인정보가 오가는 서비스라면 위험합니다. 특히 credentials를 함께 쓰는 요청에서는 와일드카드 Origin과 쿠키 전달이 함께 동작하지 않습니다.
안전한 방식은 실제 서비스에서 접근할 도메인을 목록으로 관리하는 것입니다. 예를 들어 고객용 웹은 https://www.example.com, 관리자 웹은 https://admin.example.com, 로컬 개발은 http://localhost:3000처럼 명시적으로 허용합니다. 프로그래밍 관점에서 이런 정책 분리는 단순 설정이 아니라 서비스 경계와 보안을 코드로 표현하는 일입니다. 용어 배경은 프로그래밍 개념 설명을 참고하면 이해가 쉽습니다.
- Access-Control-Allow-Origin: 요청을 허용할 정확한 출처를 응답합니다.
- Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS 등 허용할 메서드를 지정합니다.
- Access-Control-Allow-Headers: Content-Type, Authorization 등 클라이언트가 보낼 수 있는 헤더를 지정합니다.
- Access-Control-Allow-Credentials: 쿠키와 인증 정보를 허용할 때 true로 설정합니다.
Express, Spring, Nginx에서 자주 나는 실수
Node.js Express에서는 cors 미들웨어를 라우터보다 앞에 등록하지 않아 특정 경로에서만 오류가 나는 경우가 많습니다. Spring Boot에서는 전역 CORS 설정과 Spring Security 필터 설정이 따로 놀면서 OPTIONS 요청이 인증 필터에 막히기도 합니다. Nginx를 리버스 프록시로 쓰는 경우에는 애플리케이션 서버가 올바른 헤더를 내려도 Nginx 설정에서 덮어쓰거나 누락되는 일이 있습니다.
다음 체크리스트를 보면 어디부터 볼지 감이 잡힙니다. 특히 서버 로그에는 정상 응답이 찍히는데 브라우저만 실패한다면, 서버가 응답을 만들었더라도 브라우저가 헤더를 보고 차단했을 가능성이 큽니다.
- OPTIONS 요청이 200 또는 204로 응답하는지 확인합니다.
- CORS 미들웨어가 인증 미들웨어보다 먼저 실행되는지 확인합니다.
- 리버스 프록시가 CORS 헤더를 제거하거나 중복 추가하지 않는지 확인합니다.
- 운영 도메인에 www가 붙는 경우와 붙지 않는 경우를 구분해 등록합니다.
- HTTPS와 HTTP를 혼동하지 않습니다. 프로토콜이 다르면 출처도 다릅니다.
전문가 조언: CORS 허용 목록은 코드에 하드코딩하기보다 환경 변수나 설정 파일로 관리하세요. 배포 환경이 늘어날수록 보안 검토와 롤백이 쉬워집니다.
Preflight 요청 실패를 단계별로 해결하기
OPTIONS 요청은 본 요청의 예고편입니다
브라우저는 일부 요청을 보내기 전에 먼저 OPTIONS 메서드로 서버에 물어봅니다. 이 요청을 preflight라고 부르며, 서버가 해당 Origin, Method, Header를 허용한다고 답해야 실제 POST나 PUT 요청이 이어집니다. 그래서 개발자는 POST API만 만들었는데 브라우저는 OPTIONS부터 보내고, 서버가 이 메서드를 몰라 404나 405를 반환하면서 CORS 오류가 발생합니다.
이 문제는 특히 REST API를 처음 만드는 코딩 입문자에게 자주 발생합니다. 컨트롤러에는 POST /login만 구현했는데 보안 필터나 라우터가 OPTIONS /login을 막고 있으면 로그인 기능은 항상 실패합니다. 에러 메시지에는 CORS라고 뜨지만 실제 원인은 OPTIONS 처리 누락인 셈입니다.
- 상태 코드 확인: preflight 응답은 보통 200 또는 204가 적절합니다.
- Allow-Methods 확인: 실제로 보낼 POST, PUT, DELETE가 포함되어야 합니다.
- Allow-Headers 확인: Authorization이나 Content-Type이 빠지면 실패합니다.
- 캐시 설정: Access-Control-Max-Age를 설정하면 반복 preflight를 줄일 수 있습니다.
단계별 디버깅 순서
다음 순서로 보면 문제를 빠르게 좁힐 수 있습니다. 중요한 것은 프론트엔드 코드, 백엔드 코드, 프록시 설정을 한꺼번에 바꾸지 않는 것입니다. 한 번에 하나씩 확인해야 어떤 변경이 실제 해결책이었는지 알 수 있습니다.
- 개발자 도구 Network 탭에서 실패한 요청 바로 위의 OPTIONS 요청을 찾습니다.
- OPTIONS 응답 상태 코드가 2xx인지 확인합니다.
- Response Headers에 Access-Control-Allow-Origin이 현재 Origin과 일치하는지 봅니다.
- Access-Control-Allow-Methods에 실제 요청 메서드가 있는지 확인합니다.
- Access-Control-Allow-Headers에 브라우저가 보낸 요청 헤더가 모두 포함되어 있는지 비교합니다.
- 수정 후 브라우저 캐시를 비우거나 시크릿 창에서 다시 테스트합니다.
AI 코딩 도구가 CORS 설정 코드를 자동으로 제안하는 경우도 많지만, 서비스 전체 성과로 이어지려면 팀의 검증 절차가 함께 필요합니다. 실제로 AI 활용 확대와 조직 성과의 간극을 다룬 관련 기술 뉴스처럼, 자동 생성 코드는 출발점일 뿐 운영 품질은 사람이 확인해야 합니다.
배포 환경에서만 CORS가 나는 경우
로컬과 운영의 차이를 의심하세요
로컬에서는 잘 되던 API가 배포 후에만 막힌다면 환경 차이를 먼저 봐야 합니다. 개발자는 종종 localhost 기준으로 모든 테스트를 마치고, 실제 도메인에서는 HTTPS, CDN, 로드밸런서, Nginx, API 게이트웨이가 추가된다는 사실을 놓칩니다. 이 중 하나라도 Origin 헤더나 응답 헤더 처리에 관여하면 CORS 정책이 달라질 수 있습니다.
예를 들어 프론트엔드는 https://app.example.com에서 뜨는데 API는 https://api.example.com으로 호출한다면 같은 회사 서비스라도 브라우저 기준으로는 다른 출처입니다. 또 CDN 캐시가 예전 CORS 헤더를 들고 있으면 서버를 고쳐도 사용자는 계속 오류를 볼 수 있습니다. 배포 후 검증에서는 코드뿐 아니라 인프라 레이어까지 확인해야 합니다.
- CDN 캐시: 오래된 응답 헤더가 캐시되어 있을 수 있으므로 캐시 무효화를 확인합니다.
- 로드밸런서: 특정 서버 인스턴스만 설정이 달라 간헐적으로 실패할 수 있습니다.
- API 게이트웨이: 게이트웨이 레벨 CORS 설정과 애플리케이션 설정이 충돌할 수 있습니다.
- 도메인 리다이렉트: http에서 https로 이동하는 과정에서 preflight가 실패할 수 있습니다.
운영 배포 전 점검표
팀 프로젝트라면 CORS 점검을 배포 체크리스트에 넣는 것이 좋습니다. 특히 로그인, 회원가입, 파일 업로드, 결제, 관리자 API처럼 인증과 민감 데이터가 관련된 기능은 단순 조회 API보다 검증 범위를 넓혀야 합니다.
- 운영 프론트엔드 도메인 전체를 허용 목록에 등록했는지 확인합니다.
- 스테이징, 프리뷰 배포, 관리자 도메인을 별도로 구분합니다.
- 쿠키 기반 인증이라면 SameSite, Secure, Domain 속성을 함께 확인합니다.
- 모바일 웹뷰에서 호출하는 Origin이 일반 브라우저와 다른지 테스트합니다.
- 에러 모니터링 도구에 CORS 관련 네트워크 실패 로그를 남깁니다.
실무에서는 모든 Origin을 열어 두는 방식보다, 필요한 도메인을 명시하고 변경 이력을 남기는 방식이 장기적으로 유리합니다. 보안팀이 없는 작은 프로젝트라도 이 습관을 들이면 나중에 협업자가 늘어났을 때 정책을 설명하기 쉽습니다.
이것만은 꼭 기억하세요: CORS 해결 체크리스트
빠른 해결보다 원인 분리가 먼저입니다
CORS 오류는 겉으로는 비슷해 보여도 원인은 다양합니다. 프론트엔드 요청 주소가 틀린 경우, 백엔드 헤더가 빠진 경우, OPTIONS 요청이 막힌 경우, 쿠키 정책이 맞지 않는 경우, 프록시나 CDN이 헤더를 바꾸는 경우가 모두 같은 문구로 보일 수 있습니다. 그래서 웹개발 디버깅에서는 오류 메시지를 복사해 검색하기 전에 요청 흐름을 직접 확인하는 습관이 중요합니다.
아래 표처럼 증상별로 의심 지점을 정리해 두면 반복 작업을 줄일 수 있습니다. 팀 위키나 프로젝트 README에 이 내용을 넣어 두면 새로 합류한 개발자도 같은 문제로 시간을 오래 쓰지 않습니다.
- No Access-Control-Allow-Origin: 서버가 현재 Origin을 허용하지 않았을 가능성이 큽니다.
- Method not allowed: OPTIONS 또는 실제 요청 메서드가 서버에서 막혔는지 확인합니다.
- Request header field authorization is not allowed: Allow-Headers에 Authorization이 빠졌을 수 있습니다.
- Credentials flag is true: 쿠키 요청인데 와일드카드 Origin을 쓰고 있지 않은지 확인합니다.
- 로컬만 성공: 프록시 덕분에 성공한 것인지, 실제 운영 CORS가 준비된 것인지 구분합니다.
현장에서 바로 쓰는 최종 점검 순서
마지막으로 문제를 만났을 때 바로 적용할 수 있는 순서를 남깁니다. 이 순서는 React, Vue, Next.js 같은 프론트엔드 프로젝트와 Express, Spring Boot, NestJS, Django 같은 백엔드 프로젝트에 공통으로 적용할 수 있습니다.
- 브라우저 콘솔의 CORS 문구를 확인하되, 판단은 Network 탭에서 합니다.
- Request Headers의 Origin과 Response Headers의 Allow-Origin을 비교합니다.
- 인증 요청이라면 credentials 설정과 Allow-Credentials 값을 함께 확인합니다.
- OPTIONS 요청이 있는지 보고, 있다면 응답 코드와 허용 메서드, 허용 헤더를 검증합니다.
- 로컬 프록시로만 해결하지 말고 운영 도메인 기준 테스트를 별도로 진행합니다.
- 수정한 CORS 정책은 README나 배포 문서에 기록해 다음 장애를 줄입니다.
CORS는 처음에는 복잡해 보이지만 결국 브라우저, 프론트엔드 요청, 백엔드 응답, 배포 인프라가 약속을 맞추는 문제입니다. 이 흐름을 이해하면 단순히 오류를 없애는 수준을 넘어, 더 안전하고 예측 가능한 웹개발 환경을 만들 수 있습니다.

- 이전글코딩 포트폴리오 망치는 실수 총정리 가이드 26.07.03
- 다음글Docker Compose 웹개발 환경 구축 실제 사용 가이드 26.07.01
등록된 댓글이 없습니다.
