로컬에서는 되는데 배포 뒤 막히는 웹개발 CORS 오류 해결법

profile_image
작성자 웹디버거 강이든
댓글 0건 조회 4회

개발자 도구에는 빨간 오류가 가득한데 서버 로그에는 정상 요청처럼 보이나요? 로컬 웹개발 환경에서 잘 되던 로그인, 파일 업로드, API 호출이 배포 직후 실패한다면 코드 문법보다 CORS 설정과 프리플라이트 요청부터 살펴봐야 합니다. 특히 프론트엔드와 API의 도메인·포트·프로토콜 가운데 하나라도 달라지면 브라우저는 이를 서로 다른 출처로 판단합니다.

CORS 오류는 메시지가 비슷해도 원인이 다양합니다. 허용 출처 누락, OPTIONS 응답 실패, 인증 쿠키 정책, 리버스 프록시의 헤더 제거, CDN 캐시까지 점검해야 하므로 무작정 헤더 한 줄을 추가하면 오히려 보안 구멍이 생길 수 있습니다. 아래 순서대로 확인하면 문제 발생 지점을 브라우저, 애플리케이션 서버, 프록시 계층 가운데 하나로 빠르게 좁힐 수 있습니다.

배포 주소가 바뀐 순간 출처부터 다시 계산합니다

Origin은 도메인 이름만 뜻하지 않습니다

브라우저에서 말하는 출처는 프로토콜, 호스트, 포트의 조합입니다. 예를 들어 https://app.example.comhttps://api.example.com은 서브도메인이 다르므로 교차 출처 요청입니다. 같은 호스트라도 http와 https가 다르거나 443과 8443처럼 포트가 다르면 역시 별도 출처로 취급됩니다. 로컬의 프론트엔드 개발 서버가 API 요청을 대신 전달해 주는 프록시를 사용했다면, 개발 중에는 이 차이가 가려졌다가 실제 배포 환경에서 처음 드러날 수 있습니다.

CORS는 서버 연결 자체를 가능하게 만드는 기능이 아니라, 서버가 보낸 응답을 브라우저의 프론트엔드 코드에 공개해도 되는지 결정하는 규칙입니다. 그래서 Network 탭에는 200 응답이 찍혔는데 자바스크립트에서는 응답 본문을 읽지 못하는 상황도 생깁니다. 반대로 DNS 오류, TLS 인증서 오류, 서버의 500 응답까지 모두 CORS라고 단정하면 디버깅 방향이 어긋납니다. 기본 용어가 낯설다면 프로그래밍의 개념과 처리 과정을 먼저 확인하면 요청과 응답을 나누어 이해하기 쉽습니다.

가장 먼저 브라우저 개발자 도구의 Network 탭에서 실패 요청을 선택하세요. Request Headers의 Origin, 응답의 Access-Control-Allow-Origin, 요청 URL과 상태 코드를 한 화면에서 비교합니다. 콘솔 문구만 복사해 검색하는 것보다 실제 헤더를 확인해야 리디렉션이나 프록시 개입까지 찾을 수 있습니다. Origin 값이 예상한 운영 주소와 다르다면 서버 코드보다 환경 변수와 배포 URL 설정을 먼저 수정해야 합니다.

  • 프로토콜 확인: 프론트와 API가 모두 HTTPS인지 보고, HTTP에서 HTTPS로 이동하는 중간 리디렉션도 확인합니다.
  • 호스트 확인: www 포함 여부, 미리보기 배포 도메인, 사용자 지정 도메인을 각각 별도 출처로 기록합니다.
  • 포트 확인: 개발 서버의 3000·5173 포트와 운영의 443 포트를 같은 값으로 착각하지 않습니다.
  • 재현 범위 확인: curl이나 서버 간 호출은 성공하지만 브라우저만 실패한다면 CORS 가능성이 커집니다.
  • 요청 경로 확인: 최종 API 주소가 아니라 인증 서버나 스토리지 주소로 리디렉션되는지도 살펴봅니다.
운영 주소를 눈으로 추측하지 말고 실패 요청의 Origin 헤더를 그대로 복사하십시오. CORS 허용 목록은 개발자가 생각한 주소가 아니라 브라우저가 실제로 보낸 주소와 일치해야 합니다.

OPTIONS가 실패한다면 본 요청 코드는 아직 실행되지 않았습니다

단순 요청과 프리플라이트 요청을 구분합니다

브라우저는 위험 가능성이 있는 교차 출처 요청을 바로 보내지 않고, 먼저 OPTIONS 방식으로 서버의 허용 범위를 묻습니다. JSON 전송을 위해 Content-Type: application/json을 사용하거나 Authorization 헤더를 붙이고, PUT·PATCH·DELETE 같은 메서드를 호출하면 프리플라이트가 발생하는 경우가 많습니다. 이 OPTIONS 요청이 401, 403, 404, 405 또는 500으로 끝나면 실제 POST나 PATCH 처리 함수에는 진입조차 하지 않습니다.

흔한 실수는 모든 경로에 인증 미들웨어를 적용하면서 OPTIONS에도 액세스 토큰을 요구하는 것입니다. 프리플라이트에는 본 요청의 인증 정보가 기대한 형태로 실리지 않을 수 있으므로 OPTIONS를 먼저 처리하고, 허용할 출처·메서드·헤더를 명시한 뒤 성공 상태로 응답해야 합니다. 라우터에는 OPTIONS 처리를 추가했지만 Nginx, API 게이트웨이, 웹 애플리케이션 방화벽이 그 앞에서 메서드를 차단하는 경우도 있으므로 계층별 로그 시간을 비교해야 합니다.

아래 순서로 테스트하면 수정 위치를 빠르게 결정할 수 있습니다. 브라우저 Network 탭에서 동일한 URL에 OPTIONS와 본 요청이 함께 나타나는지 확인하고, OPTIONS 응답 헤더를 펼쳐 실제 허용값을 읽습니다. 코딩은 명령을 기계가 처리할 수 있는 형태로 표현하는 작업이므로, 코딩의 기본 정의처럼 입력과 처리 결과를 분리해서 보는 습관이 오류 분석에도 도움이 됩니다.

  1. 1단계: OPTIONS 상태 코드가 200 또는 204인지 확인합니다. 301·302라면 최종 이동 주소에서도 CORS 헤더가 유지되는지 봅니다.
  2. 2단계: Access-Control-Allow-Origin 값이 요청의 Origin과 정확히 일치하는지 확인합니다.
  3. 3단계: Access-Control-Allow-Methods에 실제 사용할 POST, PUT, PATCH, DELETE가 포함됐는지 확인합니다.
  4. 4단계: Access-Control-Allow-Headers에 Content-Type, Authorization, X-Request-Id 같은 사용자 정의 헤더가 빠지지 않았는지 봅니다.
  5. 5단계: OPTIONS를 통과한 뒤 본 요청이 별도로 실패하면 인증, 입력 검증, 비즈니스 로직 문제로 조사 범위를 옮깁니다.
관찰 결과가능성이 큰 원인먼저 수정할 위치
OPTIONS 404 또는 405라우트나 프록시가 OPTIONS를 처리하지 않음서버 라우터·리버스 프록시
OPTIONS 401 또는 403인증 미들웨어가 사전 요청을 차단인증 순서·게이트웨이 정책
OPTIONS 성공, 본 요청 401토큰 또는 쿠키가 전달되지 않음클라이언트 인증 설정
상태 코드는 200, 응답 접근 실패허용 출처 헤더 누락 또는 불일치응답 헤더 생성 계층

로그인과 쿠키가 얽히면 별표 허용으로 해결되지 않습니다

Credentials 설정은 브라우저와 서버가 한 쌍입니다

세션 쿠키를 사용하는 로그인 요청은 일반 공개 API보다 조건이 까다롭습니다. 프론트엔드의 fetch에는 credentials: include, Axios에는 withCredentials: true가 필요하고, 서버는 Access-Control-Allow-Credentials를 true로 보내야 합니다. 이때 Access-Control-Allow-Origin을 별표(*)로 설정할 수 없으며, 요청한 출처를 검증한 뒤 구체적인 주소를 반환해야 합니다. 한쪽만 설정하면 로그인 응답은 성공해도 다음 요청에서 세션이 사라진 것처럼 보일 수 있습니다.

쿠키 속성도 함께 확인해야 합니다. 서로 다른 사이트 맥락에서 쿠키를 전송하려면 일반적으로 SameSite=None과 Secure가 요구되며, Secure 쿠키는 HTTPS 연결을 전제로 합니다. 다만 서브도메인이 다르다고 항상 교차 사이트인 것은 아니고, 출처와 사이트의 판정 기준도 동일하지 않습니다. 따라서 CORS 허용 여부와 쿠키의 SameSite 정책을 하나의 설정으로 생각하지 말고 각각 검사해야 합니다. 브라우저 Application 탭에서 쿠키가 저장됐는지, 차단 사유가 표시되는지, Domain과 Path 범위가 요청 주소에 맞는지 확인하세요.

보안을 위해 허용 출처 목록은 환경별로 분리하는 편이 좋습니다. 운영 서버에 localhost 전체를 남겨 두거나, Origin 문자열에 특정 단어가 포함되는지만 검사하면 공격자가 비슷한 도메인을 만들어 통과할 수 있습니다. 완전 일치 비교를 기본으로 하고 미리보기 배포처럼 주소가 동적으로 생기는 경우에만 엄격한 정규식과 도메인 끝부분 검증을 사용합니다. 프로그램이 명령의 집합으로 동작한다는 설명은 프로그래밍 관련 지식백과에서도 확인할 수 있는데, CORS 역시 헤더 하나가 아니라 브라우저와 서버 설정의 조합으로 이해해야 합니다.

  • 공개 읽기 API: 쿠키와 인증 헤더가 전혀 없다면 필요한 경로에 한해 별표 허용을 검토할 수 있습니다.
  • 세션 로그인 API: 정확한 Origin, Allow-Credentials, 클라이언트 credentials 옵션을 모두 맞춥니다.
  • 토큰 인증 API: Authorization 헤더를 허용 목록에 넣고 OPTIONS가 인증 미들웨어보다 먼저 처리되게 합니다.
  • 다중 운영 도메인: 요청 Origin을 서버의 화이트리스트와 완전 일치로 비교한 뒤 같은 값을 응답합니다.
  • 쿠키 점검: SameSite, Secure, Domain, Path, 만료 시간과 브라우저의 차단 사유를 함께 기록합니다.
Access-Control-Allow-Origin: *는 만능 해결책이 아닙니다. 인증 정보가 있는 요청에서는 동작하지 않을 뿐 아니라, 비공개 응답을 의도치 않은 웹페이지에 공개할 위험도 키웁니다.

프록시와 CDN이 응답 헤더를 바꾸는지도 봅니다

애플리케이션 서버에서 올바른 헤더를 출력해도 배포 뒤에는 Nginx, 로드 밸런서, 서버리스 플랫폼, CDN이 응답을 변경할 수 있습니다. 특히 오류 응답에만 CORS 헤더가 빠지면 브라우저에는 CORS 문제로 표시되지만 실제 원인은 서버의 500 오류일 수 있습니다. 정상 응답과 실패 응답을 각각 확인하고, 프록시 설정이 기존 Access-Control-Allow-Origin을 덮어쓰거나 중복으로 추가하지 않는지도 점검합니다.

  • 동적 Origin을 반환한다면 캐시에 Vary: Origin이 반영되는지 확인합니다.
  • CDN 캐시를 비운 뒤 시크릿 창에서 다시 요청해 오래된 헤더의 영향을 배제합니다.
  • 같은 헤더가 서버와 프록시 양쪽에서 추가되어 값이 두 개가 되지 않게 책임 계층을 하나로 정합니다.

혼자 만드는 서비스와 팀 운영 서비스는 수정 위치가 다릅니다

빠른 복구 뒤 재발 방지 장치를 남깁니다

수정한 뒤에는 콘솔의 빨간 문구가 사라졌다는 사실만 확인하지 마세요. 허용한 운영 출처에서는 정상 동작하고, 허용하지 않은 임의 출처에서는 응답 접근이 차단되는지 둘 다 테스트해야 합니다. 로그인 서비스라면 새로 로그인한 직후와 페이지 새로고침 후의 세션 유지, 토큰 만료 뒤 재발급, 로그아웃 뒤 쿠키 제거까지 이어서 확인합니다. POST만 고쳤다가 며칠 뒤 파일 삭제용 DELETE에서 같은 오류가 재발하는 경우도 흔합니다.

재현 가능한 최소 테스트를 저장하면 다음 배포 때 시간을 크게 줄일 수 있습니다. 프론트엔드 통합 테스트에는 실제 운영과 동일한 Origin 조건을 만들고, 서버 테스트에는 허용 출처와 거부 출처를 각각 넣어 응답 헤더를 검증합니다. 배포 환경 변수의 끝에 슬래시가 붙거나 대소문자가 달라지는 단순 실수도 있으므로 허용 목록을 시작 로그에 안전하게 표시하되, 토큰이나 쿠키 값은 절대 로그에 남기지 않습니다.

개인 프로젝트를 운영하는 독자라면 프론트와 API를 같은 도메인의 경로로 연결하는 리버스 프록시 구성이 관리하기 편합니다. 예를 들어 화면은 /, API는 /api로 제공하면 교차 출처 설정 자체를 줄일 수 있으며, 불가피한 외부 스토리지 요청만 좁게 허용하면 됩니다. 반면 여러 팀과 여러 클라이언트가 함께 쓰는 서비스의 독자라면 출처 화이트리스트를 중앙 설정으로 관리하고, 게이트웨이에서 OPTIONS 정책과 Vary: Origin 캐시 동작을 자동 테스트하는 선택이 적합합니다. 전자는 구성 단순화에, 후자는 변경 이력과 정책 일관성에 우선순위를 두세요.

  1. 개인 서비스 권장 흐름: 동일 출처 프록시 구성 → 외부 API만 제한적으로 허용 → 브라우저에서 로그인·업로드 재검증 순서로 진행합니다.
  2. 팀 서비스 권장 흐름: 개발·스테이징·운영 허용 목록 분리 → 게이트웨이 정책 코드화 → 허용·거부 Origin 자동 테스트를 배포 단계에 넣습니다.
  3. 공통 검증: OPTIONS와 본 요청의 상태 코드, 응답 헤더, 쿠키 저장 여부를 한 건의 요청 ID로 연결합니다.
  4. 보안 검증: 임의 도메인, 유사 도메인, null Origin 요청이 예상대로 거부되는지 확인합니다.
  5. 운영 검증: CDN 캐시 갱신 후 모바일 브라우저와 데스크톱 브라우저에서 같은 시나리오를 다시 실행합니다.

로컬에서는 되는데 배포 뒤 막히는 웹개발 CORS 오류 해결법

댓글목록

등록된 댓글이 없습니다.