프론트엔드 개발하다 보면 십중팔구 만나는 그 빨간 에러.
Access to fetch at 'http://localhost:8080/api/users' from origin
'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.로컬에서 프론트 띄우고 API 좀 불렀을 뿐인데 요청이 막힌다. 오늘은 이 에러를 개발 환경에서 가장 깔끔하게 넘기는 방법, dev server proxy를 다룬다. Vite와 Webpack 둘 다 정리한다.
먼저: 이게 왜 나는지 딱 한 줄만 알고 가자
브라우저에는 동일 출처 정책(Same-Origin Policy)이라는 보안 규칙이 있다. 쉽게 말해, "지금 보고 있는 페이지의 출처(origin)와 다른 출처로 요청을 보내면 브라우저가 기본적으로 막는다"는 거다.
출처(origin)는 프로토콜 + 호스트 + 포트 세 개로 정해진다. 그래서 이 둘은 서로 다른 출처다.
프론트: http://localhost:5173 ← Vite 개발 서버
백엔드: http://localhost:8080 ← API 서버
↑ 포트가 다름 = 다른 출처 = CORS 발생localhost로 같아 보여도 포트가 다르면 다른 출처다. 그래서 개발할 때 이 에러가 그렇게 자주 나는 거다.
핵심은 이거다. CORS는 브라우저가 막는 거다. 서버끼리 통신할 땐 안 난다. 이 성질을 이용하는 게 오늘의 해법이다.
해결 아이디어: 브라우저를 속이지 말고, "같은 출처처럼" 만들기
CORS는 브라우저가 "출처가 다르다"고 판단할 때 난다. 그럼 브라우저 입장에서 출처가 같아 보이게 만들면 애초에 안 난다.
dev server proxy가 하는 일이 바로 이거다.
[브라우저]
│ http://localhost:5173/api/users 로 요청
│ (프론트랑 같은 출처! → CORS 안 남)
▼
[Vite 개발 서버] ← 프록시가 여기서 요청을 가로챔
│ http://localhost:8080/api/users 로 대신 전달
▼
[백엔드 API]브라우저는 자기랑 같은 출처(localhost:5173)로 요청을 보낸다. 그럼 CORS가 안 난다. 그 요청을 개발 서버가 받아서, 뒤에서 진짜 백엔드로 대신 전달한다. 이 서버 간 전달은 브라우저가 관여하지 않으니 CORS와 무관하다.
즉, 프론트는 자기 자신한테 요청하는 것처럼 쓰고, 실제 전달은 개발 서버가 몰래 해주는 방식이다. 우회 트릭이 아니라 Vite/Webpack이 공식적으로 제공하는 개발용 기능이다.
Vite에서 설정하기
vite.config.js (또는 .ts)의 server.proxy에 설정한다.
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
// '/api'로 시작하는 요청을 백엔드로 전달
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
이제 프론트 코드에서는 백엔드 주소를 직접 쓰지 않고, /api로 시작하는 상대 경로로 요청한다.
// 이렇게 하지 말고 (CORS 남)
fetch('http://localhost:8080/api/users')
// 이렇게 (프론트 자기 출처로 요청 → 프록시가 전달 → CORS 안 남)
fetch('/api/users')
fetch('/api/users')는 브라우저 입장에서 http://localhost:5173/api/users로 나간다. 같은 출처라 CORS가 안 나고, Vite가 이걸 받아서 http://localhost:8080/api/users로 넘겨준다.
옵션 설명
target: 요청을 실제로 보낼 백엔드 주소.changeOrigin: true: 전달할 때 요청의Host헤더를 target 기준으로 바꿔준다. 백엔드가 Host를 검사하는 경우가 많아서 보통 켜둔다. 웬만하면true로 두면 된다.
경로를 바꿔야 할 때 (rewrite)
백엔드에는 /api 접두어가 없고 /users로 받는다면, 전달할 때 /api를 떼줘야 한다.
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// 프론트: /api/users → 백엔드: /users
},
},
}
rewrite는 프론트에서는 /api를 붙여 구분하되, 실제 백엔드 경로에는 그걸 빼고 보내고 싶을 때 쓴다.
Webpack에서 설정하기 (webpack-dev-server)
CRA(Create React App)나 webpack 기반이면 devServer.proxy에 설정한다. 개념은 완전히 똑같다.
// webpack.config.js
module.exports = {
// ...
devServer: {
proxy: [
{
context: ['/api'],
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }, // Vite의 rewrite와 같은 역할
},
],
},
}
Vite와 이름만 조금 다르다. rewrite 대신 pathRewrite를 쓴다는 정도.
자주 하는 실수 체크리스트
1. 설정 바꾸고 서버 재시작 안 함vite.config.js나 webpack.config.js는 서버 시작할 때 한 번 읽힌다. 프록시 설정을 바꿨으면 개발 서버를 껐다 켜야 반영된다. 이거 몰라서 "설정했는데 왜 안 돼" 하는 경우가 정말 많다.
2. 프론트 코드에서 아직 절대경로로 요청함
프록시를 걸었는데 코드에서는 여전히 fetch('http://localhost:8080/...')로 부르면 프록시를 안 탄다. fetch('/api/...')처럼 상대경로로 바꿔야 한다.
3. 경로 접두어가 안 맞음
프론트는 /api/users로 부르는데 백엔드는 /users로 받는다면 rewrite/pathRewrite로 맞춰줘야 한다. 404가 뜨면 이걸 의심하자.
그런데 이건 "개발 환경"에서만이다
중요한 포인트. dev server proxy는 개발 서버(npm run dev)가 돌 때만 동작한다. 빌드해서 배포하면 개발 서버가 없으니 이 프록시도 없다.
그래서 실제 배포 환경에서는 CORS를 다른 방식으로 처리해야 한다. 보통은 이렇게 간다.
- 프론트와 백엔드를 같은 도메인에 두거나
- 앞단의 Nginx 같은 리버스 프록시가
/api를 백엔드로 넘겨주게 하거나 - 백엔드에서 CORS를 정식으로 열어주거나
즉, dev server proxy는 "개발할 때 편하게 넘기는 용도"고, 운영 환경 CORS는 별개로 챙겨야 한다는 걸 기억하자. (배포 환경 CORS 처리는 나중에 따로 다뤄보겠다.)
정리
- 로컬 CORS는 프론트와 백엔드의 포트가 달라서 난다. (다른 출처)
- dev server proxy는 프론트가 자기 출처로 요청하게 하고, 개발 서버가 그걸 백엔드로 대신 전달한다. 그래서 브라우저 입장에서 CORS가 안 난다.
- Vite는
server.proxy, Webpack은devServer.proxy에 설정한다. (rewrite↔pathRewrite만 이름이 다름) - 설정 후 서버 재시작, 코드에서 상대경로 사용, 경로 접두어 확인 이 세 가지가 실수 포인트.
- 이건 개발 환경 전용이다. 운영 환경 CORS는 별도로 처리해야 한다.
로컬에서 CORS 에러 만나면 백엔드 건드리기 전에 dev server proxy부터 걸어보자. 개발 흐름을 안 끊고 가장 빠르게 넘어가는 방법이다.