프록시로 CORS 우회하기
왜 이 문제를 만났나
프론트엔드를 개발하면서 외부 API를 호출했는데, 갑자기 CORS 에러가 터졌습니다:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
API는 분명 작동하는데, 브라우저에서만 막혔습니다. Postman이나 curl로 요청하면 잘 됐습니다. 이게 왜 그런지 이해가 안 갔습니다.
검색해보니 "서버에서 CORS 헤더를 추가하세요"라는 답변이 많았는데, 문제는 제가 서버를 수정할 수 없는 상황이었습니다. 외부 API였거나, 백엔드 팀이 바빠서 당장 수정이 어려웠습니다.
그래서 "개발 환경에서만이라도 CORS를 우회할 방법이 없을까?"라고 고민하다가 프록시라는 해결책을 알게 됐습니다.
처음엔 뭐가 이해가 안 갔나
CORS가 뭔지는 대충 알았습니다. "브라우저가 다른 도메인 요청을 막는 거"라고 이해했습니다. 근데 이해가 안 갔던 부분들:
- 왜 Postman은 되는데 브라우저는 안 되나?
- 왜 같은 localhost인데도 포트가 다르면 CORS 에러가 나나?
- 프록시가 어떻게 CORS를 우회하나?
특히 "프록시를 쓰면 보안 문제가 생기지 않나?"라는 의문이 들었습니다. CORS가 보안을 위한 건데, 그걸 우회하는 게 괜찮은 건가?
어떤 포인트에서 이해가 됐나
이해의 전환점은 "CORS는 브라우저 정책이지, 서버 정책이 아니다"라는 걸 받아들였을 때였습니다.
CORS의 본질
CORS를 "아파트 경비"로 비유하니까 이해가 됐습니다:
- 브라우저 (경비): 외부인(다른 도메인)이 들어오려고 하면 신분증(CORS 헤더) 확인. 없으면 차단.
- 서버 (집주인): 경비에게 "이 사람은 들여보내도 돼"라고 허가증(Access-Control-Allow-Origin) 발급.
- Postman/curl (택배): 경비를 거치지 않고 직접 집주인에게 전달. CORS 체크 없음.
sequenceDiagram
participant Browser as 브라우저
participant Proxy as 프록시 서버
participant API as 외부 API
Note over Browser,API: CORS 에러 발생 시나리오
Browser->>API: GET https://api.example.com/data
API->>Browser: 200 OK (CORS 헤더 없음)
Browser->>Browser: CORS 체크 실패
Browser->>Browser: 에러 발생!
Note over Browser,API: 프록시 사용 시나리오
Browser->>Proxy: GET /api/data (같은 도메인)
Proxy->>API: GET https://api.example.com/data
API->>Proxy: 200 OK
Proxy->>Browser: 200 OK (CORS 체크 없음)
Note right of Browser: 같은 도메인이라 CORS 체크 안 함!
핵심은 CORS는 브라우저에서만 체크한다는 겁니다. 서버끼리 통신할 때는 CORS가 없습니다. 그래서 프록시 서버를 중간에 두면:
- 브라우저 → 프록시: 같은 도메인이라 CORS 체크 안 함
- 프록시 → 외부 API: 서버끼리 통신이라 CORS 체크 안 함
프레임워크별 프록시 설정
1. Vite
가장 간단합니다. vite.config.ts에 추가:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
사용 예시:
// ❌ CORS 에러
fetch('https://api.example.com/users');
// ✅ 프록시 사용
fetch('/api/users'); // → https://api.example.com/users
옵션 설명:
target: 실제 API 서버 주소changeOrigin:Host헤더를 target으로 변경 (필수!)rewrite: 경로 변환 (/api/users→/users)
2. Create React App
package.json에 추가 (간단한 경우):
{
"proxy": "https://api.example.com"
}
또는 src/setupProxy.js (복잡한 경우):
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': '', // /api 제거
},
})
);
};
주의: setupProxy.js는 재시작 필요 없이 자동 적용됩니다.
3. Next.js
next.config.js:
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
];
},
};
또는 API Routes 사용 (권장):
// pages/api/users.ts
export default async function handler(req, res) {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
res.status(200).json(data);
}
사용:
// 클라이언트
fetch('/api/users'); // Next.js API Route 호출
4. Webpack Dev Server
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};
5. Express (Node.js)
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use(
'/api',
createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
})
);
app.listen(3000);
팁
1. 여러 API 프록시
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api/v1': {
target: 'https://api1.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v1/, ''),
},
'/api/v2': {
target: 'https://api2.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v2/, ''),
},
},
},
});
2. 인증 헤더 추가
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
// 모든 요청에 API 키 추가
proxyReq.setHeader('Authorization', 'Bearer YOUR_API_KEY');
});
},
},
},
},
});
3. HTTPS API 프록시
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
secure: false, // 자체 서명 인증서 허용
},
},
},
});
4. WebSocket 프록시
export default defineConfig({
server: {
proxy: {
'/socket': {
target: 'ws://localhost:5000',
ws: true, // WebSocket 활성화
},
},
},
});
5. 조건부 프록시
export default defineConfig({
server: {
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'https://api.example.com',
changeOrigin: true,
},
},
},
});
.env.local:
VITE_API_URL=https://dev-api.example.com
프로덕션 환경 주의사항
⚠️ 프록시는 개발 환경 전용
프록시 설정은 개발 서버에서만 작동합니다. 빌드 후에는 작동하지 않습니다.
// ❌ 프로덕션에서 안 됨
fetch('/api/users'); // 404 에러!
해결책 1: 환경 변수 사용
const API_URL = import.meta.env.VITE_API_URL || '/api';
// 개발: /api/users (프록시)
// 프로덕션: https://api.example.com/users
fetch(`${API_URL}/users`);
# .env.development
VITE_API_URL=/api
# .env.production
VITE_API_URL=https://api.example.com
해결책 2: 서버에서 프록시 설정
Nginx:
location /api {
proxy_pass https://api.example.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Vercel:
// vercel.json
{
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://api.example.com/:path*"
}
]
}
보안 고려사항
1. API 키 노출 방지
// ❌ 클라이언트에 API 키 노출
fetch('/api/users', {
headers: {
'Authorization': 'Bearer SECRET_KEY', // 브라우저에서 보임!
},
});
// ✅ 서버에서 API 키 추가 (프록시 설정)
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('Authorization', `Bearer ${process.env.API_KEY}`);
});
2. Rate Limiting
프록시를 통한 요청도 제한해야 합니다:
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 최대 100 요청
});
app.use('/api', apiLimiter);
3. 허용된 도메인만
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
const origin = req.headers.origin;
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
if (!allowedOrigins.includes(origin)) {
res.status(403).send('Forbidden');
}
},
},
}
실수했던 부분들
1. changeOrigin: false
처음에 changeOrigin을 빼먹어서 프록시가 안 됐습니다.
// ❌ 문제 코드
proxy: {
'/api': {
target: 'https://api.example.com',
// changeOrigin 없음!
},
}
// ✅ 올바른 코드
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true, // 필수!
},
}
교훈: changeOrigin: true는 거의 항상 필요합니다.
2. 경로 변환 실수
// ❌ 문제 코드
proxy: {
'/api': {
target: 'https://api.example.com/api', // /api 중복!
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
// 결과: /api/users → https://api.example.com/api/users (잘못됨)
// ✅ 올바른 코드
proxy: {
'/api': {
target: 'https://api.example.com',
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
// 결과: /api/users → https://api.example.com/users (올바름)
3. 프로덕션에서 프록시 사용
빌드 후 /api/users로 요청했는데 404가 났습니다.
교훈: 프로덕션에서는 환경 변수로 전체 URL을 사용하거나, 서버에서 프록시를 설정하세요.
4. HTTPS 인증서 에러
자체 서명 인증서를 사용하는 API에 프록시하니까 에러가 났습니다.
// ✅ 해결 코드
proxy: {
'/api': {
target: 'https://self-signed.example.com',
changeOrigin: true,
secure: false, // 자체 서명 인증서 허용
},
}
주의: 프로덕션에서는 secure: true를 사용하세요.
디버깅 팁
1. 프록시 로그 확인
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('proxy error', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Sending Request:', req.method, req.url);
});
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log('Received Response:', proxyRes.statusCode, req.url);
});
},
},
},
},
});
2. Network 탭 확인
브라우저 개발자 도구 → Network 탭에서:
- 요청 URL이
/api/users인지 확인 - 응답 헤더에 CORS 관련 헤더가 있는지 확인
3. curl로 프록시 테스트
# 프록시 없이 직접 요청
curl https://api.example.com/users
# 프록시를 통한 요청
curl http://localhost:3000/api/users
한 줄 요약
CORS 에러는 개발 환경에서 프록시 설정으로 우회할 수 있지만, 프로덕션에서는 서버에서 CORS 헤더를 추가하거나 서버 프록시를 사용해야 합니다.