CORS 에러? 리액트 Proxy 설정으로 간단하게 해결하기

프론트엔드 개발자라면 누구나 한 번쯤 콘솔 창을 가득 채운 빨간색 메시지, CORS(Cross-Origin Resource Sharing) 에러와 마주하곤 합니다. 분명 API 명세서대로 요청을 보냈는데, 브라우저가 보안을 이유로 요청을 차단할 때의 막막함은 말로 다 할 수 없죠.
오늘날의 마이크로서비스 아키텍처(MSA)에서는 프론트엔드와 백엔드가 서로 다른 도메인에서 운영되는 것이 일반적입니다. 이 과정에서 브라우저의 핵심 보안 정책인 **SOP(Same-Origin Policy)**를 충족하지 못해 발생하는 CORS 이슈를, 리액트 환경에서 가장 쉽고 우아하게 해결하는 방법인 Proxy 설정에 대해 깊이 있게 살펴보겠습니다.
1. CORS와 Proxy: 왜 직접 통신이 안 될까? (Deep Dive)
SOP라는 보안의 장벽
브라우저는 기본적으로 '동일 출처 정책(SOP)'을 따릅니다. 이는 내가 접속한 사이트(A.com)의 스크립트가 허가 없이 다른 사이트(B.com)의 데이터에 접근하는 것을 막는 방어 기제입니다. 하지만 현대의 앱은 외부 API 서버와 통신이 필수적이죠. 이때 서버 측에서 "이 도메인은 안전하니 허용해 주겠다"라는 헤더(Access-Control-Allow-Origin)를 보내주어야 하는데, 개발 단계에서 이를 매번 설정하기는 번거롭습니다.
Proxy의 마법: "나는 내가 보낸 줄 알았어"
Proxy는 중간에서 요청을 가로채 전달해 주는 **'중개자'**입니다. 브라우저가 보기에는 /api로 시작하는 요청이 리액트 개발 서버(localhost:3000)로 가는 것처럼 보이지만, 사실 개발 서버가 이 요청을 뒤에서 슬쩍 실제 백엔드 서버(localhost:8080)로 전달해 주는 방식입니다.
비유하자면 이렇습니다: 해외 직구가 금지된 물건을 사고 싶을 때, 해외에 사는 친구(Proxy)에게 대신 구매해 달라고 부탁하는 것과 같습니다. 세관(브라우저)은 친구가 나에게 선물을 보내는 것으로 인식하여 아무런 제재 없이 통과시켜 주는 원리죠.
2. 실전 적용: http-proxy-middleware로 유연하게 대응하기
단순한 설정은 package.json에 proxy 필드를 추가하는 것만으로 충분하지만, 실무에서는 여러 대의 API 서버를 다루거나 특정 경로만 필터링해야 하는 경우가 많습니다. 이때 가장 권장되는 방식이 http-proxy-middleware 라이브러리를 사용하는 것입니다.
Step 1: 라이브러리 설치
npm install http-proxy-middleware --save
# 또는
yarn add http-proxy-middleware
Step 2: setupProxy.js 구성하기
src 폴더 바로 아래에 setupProxy.js 파일을 생성합니다. 리액트 스크립트는 실행 시 이 파일을 자동으로 인식하여 프록시 설정을 적용합니다.
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
// 1. '/api'로 시작하는 모든 요청을 타겟 서버로 전달
'/api',
createProxyMiddleware({
target: 'http://api.ecommerce-shop.com:8080', // 실제 백엔드 주소
changeOrigin: true, // 대상 서버 구성에 따라 호스트 헤더 변경 여부 결정
pathRewrite: {
'^/api': '', // URL에서 '/api' 부분을 제거하고 전달 (예: /api/products -> /products)
},
})
);
// 2. 필요에 따라 다른 서버(예: 인증 서버)를 추가로 설정 가능
app.use(
'/auth',
createProxyMiddleware({
target: 'http://auth.internal.system:9000',
changeOrigin: true,
})
);
};
주요 옵션 상세 설명
- target: 요청을 전달할 목적지 주소입니다.
- changeOrigin: 이 옵션을 true로 설정하면 서버가 보낸 요청의 출처를 target 주소로 위장합니다. 많은 백엔드 프레임워크가 이 헤더를 검증하므로 true 설정을 권장합니다.
- pathRewrite: 프론트엔드 코드에서는 가독성을 위해 /api를 붙여 호출하지만, 실제 서버 API 구조에는 해당 접두사가 없을 때 유용합니다.
3. 트러블슈팅: 설정했는데 왜 안 될까요?
프록시를 설정했음에도 여전히 에러가 발생한다면 다음 사항을 점검해 보세요.
- 서버 재시작 필수: setupProxy.js는 노드 런타임에서 실행됩니다. 파일을 수정했다면 반드시 리액트 개발 서버를 껐다가 다시 켜야 적용됩니다.
- 전체 URL 사용 금지: axios.get('http://api.com/api/user') 처럼 호출하면 프록시를 타지 않습니다. 반드시 상대 경로인 axios.get('/api/user')를 사용해야 브라우저가 동일 출처로 인식합니다.
- 네트워크 탭 확인: 브라우저 개발자 도구의 Network 탭에서 요청 URL이 여전히 localhost:3000/...으로 보이는 것이 정상입니다. 내부는 프록시가 처리하고 있으니까요.
4. Trade-offs: 프록시 설정이 만능일까?
장점
- 백엔드 코드 수정 없이 로컬 개발 환경에서 CORS를 즉시 해결합니다.
- 실제 운영 환경과 유사한 경로 구조를 유지할 수 있습니다.
단점 및 한계
- 개발 환경 전용: 이 설정은 npm start로 실행되는 Webpack Dev Server에서만 작동합니다. 빌드 후 배포된 프로덕션 환경(Nginx, S3 등)에서는 작동하지 않으므로, 서버 측에서 실제 CORS 설정을 하거나 Nginx의 리버스 프록시 설정을 별도로 해주어야 합니다.
5. 결론 및 제언
리액트의 Proxy 설정은 프론트엔드 개발자가 백엔드의 환경 변화에 구애받지 않고 오직 클라이언트 로직에만 집중할 수 있게 해주는 강력한 도구입니다. 특히 http-proxy-middleware를 활용한 구성은 복잡한 비즈니스 요구사항도 유연하게 처리할 수 있는 확장성을 제공하죠.
단, 프록시는 개발 편의를 위한 '우회로'임을 잊지 마세요. 궁극적으로는 보안 가이드라인에 맞춘 백엔드의 정석적인 CORS 설정이 병행되어야 합니다.
혹시 지금 진행 중인 프로젝트에서 로컬 환경과 프로덕션 환경의 API 주소를 어떻게 다르게 관리하고 계신가요? .env 파일을 활용한 환경 변수 분리 전략과 함께 사용한다면 더욱 견고한 애플리케이션을 만들 수 있을 것입니다.