
프록시로 CORS 우회하기
개발 중 CORS 에러를 프록시로 해결하는 방법과 주의사항을 정리했습니다.

개발 중 CORS 에러를 프록시로 해결하는 방법과 주의사항을 정리했습니다.
페이스북은 왜 REST API를 버렸을까? 원하는 데이터만 쏙쏙 골라 담는 GraphQL의 매력과 치명적인 단점 (캐싱, N+1 문제) 분석.

직접 가기 껄끄러울 때 프록시가 대신 갔다 옵니다. 내 정체를 숨기려면 Forward Proxy, 서버를 보호하려면 Reverse Proxy. 같은 대리인인데 누구 편이냐가 다릅니다.

백엔드: 'API 다 만들었어요.' 프론트엔드: '어떻게 써요?' 이 지겨운 대화를 끝내주는 Swagger(OpenAPI)의 마법.

빨간색 에러 메시지를 보고 당황하셨나요? 브라우저가 당신을 괴롭히는 게 아니라 보호하고 있는 겁니다.

프론트엔드를 개발하면서 외부 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가 뭔지는 대충 알았습니다. "브라우저가 다른 도메인 요청을 막는 거"라고 이해했습니다. 근데 이해가 안 갔던 부분들:
특히 "프록시를 쓰면 보안 문제가 생기지 않나?"라는 의문이 들었습니다. CORS가 보안을 위한 건데, 그걸 우회하는 게 괜찮은 건가?
이해의 전환점은 "CORS는 브라우저 정책이지, 서버 정책이 아니다"라는 걸 받아들였을 때였습니다.
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가 없습니다. 그래서 프록시 서버를 중간에 두면:
가장 간단합니다. 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)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는 재시작 필요 없이 자동 적용됩니다.
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 호출
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};
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);
// 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/, ''),
},
},
},
});
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');
});
},
},
},
},
});
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
secure: false, // 자체 서명 인증서 허용
},
},
},
});
export default defineConfig({
server: {
proxy: {
'/socket': {
target: 'ws://localhost:5000',
ws: true, // WebSocket 활성화
},
},
},
});
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*"
}
]
}
// ❌ 클라이언트에 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');
}
},
},
}
changeOrigin: false처음에 changeOrigin을 빼먹어서 프록시가 안 됐습니다.
// ❌ 문제 코드
proxy: {
'/api': {
target: 'https://api.example.com',
// changeOrigin 없음!
},
}
// ✅ 올바른 코드
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true, // 필수!
},
}
교훈: changeOrigin: true는 거의 항상 필요합니다.
// ❌ 문제 코드
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 (올바름)
빌드 후 /api/users로 요청했는데 404가 났습니다.
교훈: 프로덕션에서는 환경 변수로 전체 URL을 사용하거나, 서버에서 프록시를 설정하세요.
자체 서명 인증서를 사용하는 API에 프록시하니까 에러가 났습니다.
// ✅ 해결 코드
proxy: {
'/api': {
target: 'https://self-signed.example.com',
changeOrigin: true,
secure: false, // 자체 서명 인증서 허용
},
}
주의: 프로덕션에서는 secure: true를 사용하세요.
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);
});
},
},
},
},
});
브라우저 개발자 도구 → Network 탭에서:
/api/users인지 확인# 프록시 없이 직접 요청
curl https://api.example.com/users
# 프록시를 통한 요청
curl http://localhost:3000/api/users
CORS 에러는 개발 환경에서 프록시 설정으로 우회할 수 있지만, 프로덕션에서는 서버에서 CORS 헤더를 추가하거나 서버 프록시를 사용해야 합니다.