Vite 6.x 프록시에서 PATCH만 CORS 에러? 소문자 메서드 함정과 해결법
Vite 6.x 개발 서버 프록시 환경에서 PATCH 요청만 CORS 에러가 발생하는 원인은 HTTP 메서드 대소문자입니다. toUpperCase() 한 줄로 해결하는 방법과 프록시 디버깅 체크리스트를 정리합니다.
Vite 6.x 프록시에서 PATCH만 CORS 에러가 나는 이유
Vite 6.x 프록시 환경에서 PATCH 요청만 CORS 에러로 막힌다면, 범인은 HTTP 메서드 대소문자다.
GET도 되고, POST도 된다. PUT도 된다. 그런데 PATCH만 안 된다.
터미널에서 curl을 쏘면 200 OK가 돌아온다. 백엔드는 멀쩡하다. 브라우저에서만 터진다.
CORS 설정이 잘못된 건가 싶어 백엔드 코드를 샅샅이 뒤졌다. (백엔드 CORS 설정 과정은 NestJS CORS 삽질 총정리에서 다뤘다.) 이미 PATCH가 허용 메서드에 포함되어 있었다. 한참을 헤매고 나서야 범인이 Vite 프록시에 있다는 걸 알아냈다. 정확히는,
소문자 patch가 그대로 흘러가는 구조였다.
2시간 삽질 끝에 toUpperCase() 한 줄로 해결했다 💀
🔍 증상: PATCH 요청만 CORS 에러


관리자 페이지에서 데이터 수정 기능을 구현하던 중이었다. React 커스텀 DataProvider에서 PATCH 요청을 보내는 구조였는데, 다른 메서드는 멀쩡하게 동작했다.
| HTTP 메서드 | 결과 |
|---|---|
| GET | ✅ 정상 |
| POST | ✅ 정상 |
| PUT | ✅ 정상 |
| DELETE | ✅ 정상 |
| PATCH | ❌ CORS 에러 |
딱 PATCH만. 이 조합이 너무 이상했다.
❌ 실제 에러 메시지
Access to fetch at 'http://localhost:3000/api/v1/...'
Method patch is not allowed by Access-Control-Allow-Methods in preflight response.
처음엔 그냥 흘려봤다. 에러 메시지를 다시 읽는 순간 뭔가 이상하다는 걸 느꼈다.
Method patch — 소문자 patch다.
CORS 에러에서 메서드명이 소문자로 찍히는 건 이례적이다. HTTP 표준에서 메서드는 보통 대문자로 표기한다. 이게 핵심 단서였다.
핵심: CORS 에러 메시지에서
Method patch처럼 소문자로 찍히면, 요청 자체가 소문자 메서드를 보내고 있는 거다. 백엔드 CORS 설정을 의심하기 전에 클라이언트 코드부터 확인하자.
🔎 왜 PATCH만 걸렸나?
GET과 POST는 CORS preflight(OPTIONS 요청)를 생략할 수 있는 단순 요청(Simple Request) 조건에 해당한다. 특정 헤더 조건을 충족하면 브라우저가 그냥 통과시킨다.
반면 PATCH, PUT, DELETE는 무조건 preflight를 거친다. 브라우저가 먼저 OPTIONS 요청을 보내서 “이 메서드 써도 돼?” 하고 서버에 물어보는 것이다.
preflight 응답의 Access-Control-Allow-Methods에 대소문자가 일치하는 메서드가 없으면 브라우저가 막는다. 백엔드엔 PATCH(대문자)가 있는데,
실제로 요청된 건 patch(소문자)였으니 당연히 매칭이 안 됐다.
주의: GET, POST는 preflight 없이 통과하기 때문에 소문자 메서드 문제가 겉으로 드러나지 않는다. PATCH나 PUT에서만 이 함정이 표면화된다.
🔬 원인: Vite 6.x 프록시의 소문자 메서드 함정


HTTP 표준과 대소문자
HTTP/1.1 스펙(RFC 7230)에는 메서드가 대소문자를 구분한다고 명시되어 있다. PATCH와 patch는 엄밀히 다른 메서드다. 대부분의 브라우저와 서버는 이를 엄격하게 처리한다.
문제의 발단은 DataProvider였다. 사용 중이던 REST 라이브러리가 내부적으로 HTTP 메서드를 소문자 문자열로 관리하고 있었다. 이 소문자가 fetch 옵션에 그대로 넘어갔다.
❌ 문제의 코드
// DataProvider 내부 — 메서드를 그대로 전달
const response = await httpClient(url, {
method: method, // "patch" ← 소문자 그대로
---
body: JSON.stringify(data),
headers: headers,
});
이 상태에서 Vite 6.x 개발 서버의 프록시(http-proxy 기반)는 받은 메서드를 변환 없이 그대로 백엔드에 전달한다. 대소문자를 정규화하는 로직이 없다.
결국 흐름이 이렇게 된다.
브라우저 → Vite 프록시 → 백엔드
"patch" → "patch" → "patch"
백엔드 CORS 설정은 PATCH(대문자)만 허용하고
있으니, preflight 단계에서 매칭 실패 → 거부.
팁: Vite 공식 문서 — 서버 프록시 설정에 따르면, 개발 서버 프록시는
http-proxy라이브러리를 내부적으로 사용한다. 이 라이브러리는 메서드를 대소문자로 정규화하지 않고 그대로 포워딩한다. Vite 6.x 기준으로 이 동작은 변경되지 않았다.
왜 로컬에서만 발생하나?
프로덕션 환경에서는 보통 Nginx나 클라우드 로드밸런서가 앞단에 있다. 이 레이어들은 대부분 메서드를 대문자로 정규화한다. 그래서 로컬 Vite 개발 서버에서만 재현되고, 스테이징이나 프로덕션에선 문제가 없어 보이는 경우가 생긴다.
로컬에서만 터지는 CORS 에러라면 Vite 프록시 레이어를 의심할 이유가 하나 더 생기는 것이다.
🛠️ 해결: 두 가지 수정


H3 1. HTTP 메서드 대문자 변환
가장 근본적인 수정이다. DataProvider에서 fetch를 호출하기 직전에 메서드를 대문자로 변환했다.
❌ Before
// DataProvider — 메서드 대소문자 검증 없음
const response = await httpClient(url, {
method: method, // 라이브러리에서 넘어온 소문자 그대로
---
body: JSON.stringify(data),
headers: headers as HeadersInit,
});
소문자 메서드가 그대로 흘러간다. 라이브러리 내부 동작에 의존하게 되어 취약하다.
✅ After
// DataProvider — 메서드를 명시적으로 대문자 변환
const httpMethod = method.toUpperCase(); // "patch" → "PATCH"
const response = await httpClient(url, {
method: httpMethod,
body: JSON.stringify(data),
---
headers: headers as HeadersInit,
});
toUpperCase() 한 줄 추가. 이 한 줄을 찾는 데 2시간이 걸렸다.
외부 라이브러리가 어떤 케이스로 메서드를 넘기든 상관없이, fetch 직전에 반드시 대문자로 변환해주는 것이다. 방어적 코딩의 기본이기도 하다.
팁: HTTP 클라이언트 wrapper를 직접 만든다면,
method.toUpperCase()를 내부에 한 번만 추가해두면 된다. 개별 호출부마다 신경 쓸 필요가 없어진다.
H3 2. 프록시 URL로 전환
메서드 대소문자 수정이 근본 해결이긴 하다. 그런데 이 기회에 구조 자체를 더 안전하게 바꿨다.
기존엔 .env.local에서 백엔드 URL을 직접 참조하고
있었다. 이러면 Same-Origin 정책에 걸려 CORS가 항상 필요하다. Vite 프록시를 통하면 브라우저 입장에서 Same-Origin이 되므로 CORS 자체가 불필요해진다.
❌ Before — 직접 호출
# .env.local
# 백엔드를 직접 참조 → 크로스 오리진 → CORS 필수
VITE_API_BASE_URL=http://localhost:3000/api/v1
✅ After — 프록시 경유
# .env.local
# /api 경로로 변경 → Same-Origin → CORS 불필요
VITE_API_BASE_URL=/api/v1
그리고 vite.config.ts에 프록시 설정을 추가했다.
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
---
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
---
// rewrite: (path) => path.replace(/^\/api/,
"") // 경로 변환이 필요하면 활성화
},
---
},
},
});
이렇게 하면 브라우저가 /api/v1/users를 요청해도,
Vite 개발 서버가 이를 받아 http://localhost:3000/api/v1/users로 중계한다. 브라우저 입장에선 Same-Origin 요청이라 CORS 헤더 자체가 필요 없다.
메서드 대소문자 수정 + 프록시 경유. 이중 안전장치다.
🛡️ 예방: CORS 디버깅 체크리스트

같은 삽질을 반복하지 않으려고 체크리스트로 정리해뒀다. Vite + React 프로젝트에서 CORS 에러가 터지면 이 순서로 확인한다.
📋 DataProvider 작성 시 점검 항목
- HTTP 메서드에
.toUpperCase()적용했는가? - 외부 REST 라이브러리를 쓴다면, 내부에서 소문자로 넘기는지 확인했는가?
- 커스텀 fetch wrapper가 있다면, 메서드 변환 로직이 내부에 있는가?
-
.env.local에서 백엔드를 직접 참조하고 있는가? (프록시 경유 전환 검토)
📋 CORS 에러 발생 시 디버깅 순서
-
백엔드 curl 테스트 — 백엔드 자체가 정상인지 먼저 확인
curl -X PATCH http://localhost:3000/api/v1/resource/1 \ -H "Content-Type: application/json" \
-d '{"name":"test"}' -v
```
2. DevTools Network 탭 — OPTIONS preflight 요청의 응답 헤더를 직접 확인
```
Access-Control-Allow-Methods: GET,
POST, PUT, PATCH, DELETE
```
여기에 `PATCH`가 포함되어 있는지 확인.
3. 에러 메시지 정독 — Method patch vs Method PATCH 대소문자 확인. 소문자면 클라이언트 코드 문제다.
- Vite 프록시 사용 여부 — 프록시를 쓰고 있다면 메서드 대소문자 의심, 프록시 로그 추가.
주의: DevTools에서 preflight(OPTIONS) 요청은 기본적으로 숨겨진다. “Preserve log”를 활성화하거나 필터에서 “Other”를 선택해야 보인다. preflight 응답을 못 보면 CORS 디버깅이 감으로 하는 수준이 된다.
🔧 프록시 디버깅 로그 추가
Vite 프록시에 로그를 추가하면 실제로 어떤 메서드가 전달되는지 볼 수 있다.
// vite.config.ts — 프록시 요청 로그
proxy: {
"/api": {
---
target: "http://localhost:3000",
changeOrigin: true,
configure: (proxy) => {
---
proxy.on("proxyReq", (proxyReq, req) => {
// 실제로 백엔드에 전달되는 메서드를 콘솔에 출력
console.log(`[Proxy] ${req.method} ${req.url}`);
---
});
},
},
---
},
터미널에서 [Proxy] patch /api/v1/... 처럼 소문자가 찍히면,
범인을 잡은 것이다. 대문자 PATCH가 찍혀야 정상이다.
이 로그 하나만 추가했어도 원인을 훨씬 빨리 찾을 수 있었다. 다음에 Vite 프록시 프로젝트를 세팅할 때는 개발 초기에 미리 달아두는 게 맞겠다고 느꼈다.
팁:
configure콜백은proxy.on("proxyRes", ...)이벤트도 제공한다. 백엔드에서 어떤 응답이 돌아오는지 로깅하면 CORS 헤더 디버깅에도 유용하다.
📌 정리

| 상황 | 안티패턴 | 권장 패턴 |
|---|---|---|
| HTTP 메서드 전달 | method: method (소문자 그대로) | method: method.toUpperCase() |
| 개발 환경 API URL | http://localhost:3000/api/v1 (직접 참조) | /api/v1 (Vite 프록시 경유) |
| CORS 에러 디버깅 | 백엔드 CORS 설정부터 확인 | 에러 메시지의 메서드 대소문자 먼저 확인 |
| 프록시 문제 추적 | 감으로 디버깅 | configure로 프록시 로그 추가 |
HTTP 메서드 대소문자라는 기본 중의 기본에서 2시간을 날렸다.
근데 그 덕에 CORS 에러가 뜰 때 습관이 생겼다. 에러 메시지의 메서드명을 제일 먼저 확인한다. patch냐 PATCH냐. 이 한 글자 차이가 2시간짜리 삽질이 되기도 한다 ✨