남잭슨의 개발 블로그
Vite로 개발할 때는 API가 잘 되는데 배포하자 CORS 오류가 난다면, 개발 중 사용한 프록시와 배포 후 실제 요청 주소부터 비교하세요. 그다음 API 응답의 Access-Control-Allow-Origin이 배포한 화면의 출처와 맞는지 확인합니다. 이 헤더는 API 서버가 돌려주는 응답에 있어야 합니다. 프런트엔드 요청에 같은 이름의 헤더를 추가해도 허용되는 것은 아닙니다. MDN 응답 헤더 설명
1. 화면 주소와 API 주소의 출처를 비교합니다
출처(origin)는 프로토콜·호스트·포트의 조합입니다. 같은 도메인처럼 보여도 https://app.example과 https://api.example은 호스트가 달라 다른 출처입니다. http://localhost:5173과 http://localhost:3000도 포트가 다릅니다. 반면 같은 출처에서 경로만 /notes, /users로 바뀌는 것은 출처 변경이 아닙니다. MDN 출처 정의
아래는 개발 프록시를 사용하는 앱의 설명용 예시입니다. .example 주소는 실제 호출 대상이 아닙니다.
| 상황 | 브라우저가 요청하는 곳 | 그다음 처리 |
|---|---|---|
| 로컬 개발 | localhost:5173/api/notes |
Vite 서버가 API 서버로 전달 |
| 배포 후 직접 호출 | https://api.example/notes |
API가 화면의 출처를 허용해야 함 |
| 배포 후 같은 출처로 호출 | https://app.example/api/notes |
운영 서버에 해당 경로를 처리할 API 또는 프록시 필요 |
Vite의 server.proxy는 개발 서버 설정입니다. vite build로 만든 정적 파일에 운영용 프록시 서버가 함께 생기지 않습니다. 따라서 로컬의 /api가 배포 환경에서도 자동 연결될 것이라고 가정하면 안 됩니다. 배포 서버가 그 경로를 모르면 404가 날 수도 있습니다. Vite 개발 프록시 문서
2. Console과 Network에서 실패 지점을 찾습니다
- 배포한 페이지에서 개발자 도구를 열고 Console의 첫 오류를 읽습니다.
Mixed Content나 인증서 오류라면 HTTPS 연결 문제부터 점검합니다. - Network를 연 상태에서 API를 호출하는 동작을 다시 합니다. 요청을 선택해 Headers의 Request URL과 요청 헤더의 Origin을 기록합니다.
- Response Headers에서
Access-Control-Allow-Origin을 찾습니다. 화면의 출처와 프로토콜·호스트·포트가 맞는지 비교합니다. - 같은 API의 OPTIONS 요청이 보이면 그것부터 확인한 뒤 실제 GET 또는 POST 응답도 확인합니다.
Chrome에서는 요청을 선택한 뒤 Headers에서 요청·응답 헤더를 나누어 볼 수 있습니다. 쿠키나 토큰이 담긴 전체 헤더를 공개 게시물에 복사하지 말고, 진단에 필요한 출처와 허용 헤더만 남기세요. Chrome Network 헤더 확인 방법
3. OPTIONS 성공과 실제 응답 성공을 따로 봅니다
브라우저는 JSON POST처럼 조건에 해당하는 교차 출처 요청을 보내기 전에, OPTIONS로 메서드와 헤더의 허용 여부를 확인합니다. 이를 프리플라이트라고 합니다. 모든 요청에 OPTIONS가 생기는 것은 아니며, 허용 결과가 캐시되어 보이지 않을 수도 있습니다. MDN 프리플라이트 설명
다음은 쿠키 없는 JSON POST의 설명용 교환입니다. 경로와 허용값은 실제 서비스의 요구에 맞춰야 합니다.
OPTIONS /notes HTTP/1.1
Origin: https://app.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Vary: Origin
여기서 204만 반환하고 허용 헤더를 빠뜨리면 충분하지 않습니다. OPTIONS에서 인증 미들웨어가 401을 내는지도 확인하세요. 필요한 API의 프리플라이트를 처리하면서, 실제 데이터 요청의 인증은 유지해야 합니다. 이어지는 실제 POST 응답에도 출처 허용이 필요합니다.
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example
Vary: Origin
{"saved":true}
여러 화면 주소를 허용한다면 서버에서 허용 목록과 요청의 Origin을 비교한 뒤 해당 출처 하나를 반환합니다. 요청값을 무조건 되돌려주거나 여러 출처를 쉼표로 이어 붙이는 방식은 피하세요. 출처에 따라 응답이 달라지면 캐시도 이를 구별하도록 Vary: Origin을 설정합니다. MDN 출처 허용과 캐시
4. 로그인 쿠키를 쓰면 추가 조건을 확인합니다
로그인 쿠키를 포함하는 교차 출처 요청은 별도로 봐야 합니다. credentials: 'include'를 쓰는 경우 서버는 구체적인 허용 출처와 Access-Control-Allow-Credentials: true를 응답해야 하며, 허용 출처에 *를 쓸 수 없습니다. 쿠키의 SameSite와 브라우저 쿠키 정책도 적용되므로 CORS 헤더만 맞췄다고 로그인이 보장되지는 않습니다. 자격 증명 허용 · Fetch의 쿠키 조건
mode: 'no-cors'는 JSON을 읽기 위한 해결책이 아닙니다. 교차 출처 응답이 불투명한 응답이 되어 JavaScript에서 본문과 헤더를 읽을 수 없기 때문입니다. MDN 요청 모드 설명
5. 증상에 따라 수정할 곳을 고릅니다
| 관찰한 증상 | 다음 확인 |
|---|---|
| OPTIONS가 401·403·404 | API 경로, 프리플라이트 처리, 앞단 게이트웨이의 응답 |
| OPTIONS는 정상, 실제 응답에서 CORS 오류 | 실제 응답과 오류 응답의 출처 허용 헤더, 중복된 헤더 |
| HTTP 200인데 앱에서 본문을 못 읽음 | 상태 코드 외에 CORS 헤더와 Console 오류를 확인 |
| API 주소가 undefined 또는 예상과 다름 | Vite 환경변수와 배포된 코드의 실제 주소 확인 |
| HTTPS 화면에서 HTTP API가 차단됨 | Mixed Content 확인과 API의 HTTPS 적용 순서 |
직접 관리하는 API라면 애플리케이션·프록시·게이트웨이 중 실제 응답을 만드는 곳에서 수정합니다. 외부 API라면 제공자의 브라우저 호출 지원과 출처 등록 기능부터 확인하세요. 지원하지 않으면 이용 조건에 맞는 서버 연동이나 다른 API가 필요합니다. 직접 중계 서버를 만들 때는 대상 API와 경로를 제한하고, 사용자가 임의 URL을 전달할 수 있는 공개 프록시로 만들지 않습니다.
이번 확인 범위와 수정 후 점검
macOS·Node.js 22.23.3의 로컬 HTTP 예제에서 응답 헤더 네 조건을 확인했습니다. 이어서 Chrome 155에서 127.0.0.1의 서로 다른 두 포트를 화면과 API로 사용해, 브라우저가 응답을 읽을 수 있는지도 비교했습니다.

CORS 네 조건의 실제 실행 결과. Chrome에서 실제 Fetch 요청을 실행한 화면입니다. 허용 헤더 없음·다른 출처 허용은 읽기 차단, 현재 출처 허용은 읽기 성공이었습니다. JSON POST는 OPTIONS 204 뒤 POST 200을 받았습니다. 숫자는 로컬 서버 요청 기록과 대조했습니다.
| API 응답 조건 | 서버 응답 | 브라우저에서 확인한 결과 |
|---|---|---|
| 출처 허용 헤더 없음 | 200 | fetch 실패, JSON 읽기 불가 |
| 현재 화면의 출처 허용 | 200 | JSON 읽기 성공 |
| JSON POST의 사전 요청과 출처 허용 | OPTIONS 204 → POST 200 | 자동 사전 요청 후 JSON 읽기 성공 |
| 현재 화면과 다른 출처만 허용 | 200 | fetch 실패, JSON 읽기 불가 |
서버의 200 응답과 브라우저가 그 본문을 읽을 수 있는지는 별개라는 점을 로컬에서 확인한 실습입니다. 로그인 쿠키·인증·TLS·Vite 프록시·실제 배포 환경은 검증 범위에 포함하지 않았습니다.
수정 후에는 원래 문제가 난 배포 페이지에서 같은 동작을 다시 실행하세요. Console 오류가 사라졌는지, 실제 API 응답을 앱이 읽는지, 로그인 기능이 있다면 의도한 사용자에게만 동작하는지까지 확인하면 됩니다.
'개발일지' 카테고리의 다른 글
| Docker Desktop에서 호스트 DB 연결 안 될 때: localhost 확인 (0) | 2026.10.28 |
|---|---|
| HTTPS 사이트에서 HTTP API 호출이 막힐 때: Mixed Content 해결 순서 (0) | 2026.10.28 |
| Vite 환경변수가 배포 후 undefined일 때: VITE_와 빌드 시점 확인 (0) | 2026.10.26 |
| OpenSSL로 SSL 인증서 만료일과 도메인(SAN) 확인하기 (0) | 2026.10.25 |
| Vite localhost 모바일 접속 안됨: --host 설정과 확인 순서 (0) | 2026.10.24 |
