프론트엔드 개발을 처음 배우고 백엔드 API와 통신을 연결하던 날, 데이터가 화면에 멋지게 뜰 것이라는 기대와 달리 브라우저 콘솔창에는 시뻘건 에러 메시지가 떴습니다.
Access to fetch at 'http://localhost:5000/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
분명 포스트맨(Postman)으로 테스트했을 때는 데이터가 아주 잘 넘어왔는데, 브라우저에서 fetch나 axios로 요청만 날리면 무조건 에러가 터지는 기이한 현상에 밤을 새워가며 삽질했던 기억이 납니다.
오늘은 초보 개발자들의 멘탈을 흔드는 CORS(Cross-Origin Resource Sharing)의 진짜 동작 원리와, 실무 및 개발 환경에서 이를 깔끔하게 해결하는 3가지 방법을 정리해 봅니다.
1. 내가 마주친 문제: "Postman에서는 되는데 왜 브라우저에서만 안 될까?"
당시 React 개발 서버는 http://localhost:3000에서 돌고 있었고, 백엔드 Expres
s 서버는 http://localhost:5000에서 돌고 있었습니다.
- Postman 요청 시: 상태 코드 200 OK와 함께 유저 데이터가 정상 수신됨.
- 리액트 앱에서 요청 시: 네트워크 요청 실패 및 빨간색 CORS 에러 발생.
💡 CORS는 '서버의 에러'가 아니라 '브라우저의 보안 정책'이다!
많은 분들이 CORS 에러가 뜨면 백엔드 서버가 다운되었거나 잘못 만들어졌다고 오해합니다. 하지만 CORS 에러를 띄우는 주체는 '웹 브라우저'입니다.
웹 브라우저는 보안상의 이유로 SOP(Same-Origin Policy, 동일 출처 정책)를 따릅니다. 즉, 현재 웹 페이지가 열려 있는 출처(Origin = 프로토콜 + 도메인 + 포트번호)와 다른 출처로 데이터를 요청할 경우, 서버가 명시적으로 *"이 출처의 요청을 허용한다"*는 응답 헤더를 내려주지 않으면 브라우저가 응답을 차단해 버리는 것입니다.
2. 해결 방법 1: 백엔드 서버에서 CORS 헤더 허용하기 (가장 정석)
가장 기본적이고 정석적인 해결법은 백엔드 서버의 응답 헤더에 Access-Control-Allow-Origin을 추가해 주는 것입니다.
Node.js(Express) 환경이라면 cors 미들웨어를 설치해 간단히 해결할 수 있습니다.
// Express 백엔드 서버 설정 (server.js)
const express = require('express');
const cors = require('cors');
const app = express();
// ⭕ 특정 프론트엔드 출처(Origin)만 안전하게 허용
app.use(
cors({
origin: 'http://localhost:3000', // 프론트엔드 주소
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true, // 쿠키/인증 헤더 포함 허용
})
);
app.get('/api/users', (req, res) => {
res.json({ message: 'CORS 설정 성공!' });
});
app.listen(5000, () => console.log('백엔드 서버 가동 (Port 5000)'));
3. 해결 방법 2: 프론트엔드 개발 서버 프록시(Proxy) 설정하기
백엔드 코드를 당장 수정할 권한이 없거나, 외부 공공 API를 로컬에서 테스트해야 할 때는 프론트엔드 개발 서버의 프록시(Proxy) 기능을 활용하는 것이 최고입니다.
브라우저는 같은 출처인 localhost:3000/api로 요청을 보내고, 프론트엔드 개발 서버(Node.js)가 대신 백엔드로 요청을 전달하여 받아오기 때문에 브라우저의 CORS 검열을 완벽히 우회할 수 있습니다.
// package.json (Create React App 기준)
{
"name": "my-frontend-app",
"version": "0.1.0",
"proxy": "http://localhost:5000"
}
또는 Vite를 사용 중이라면 vite.config.ts 파일에서 아래와 같이 깔끔하게 설정할 수 있습니다.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
},
},
},
});
4. 해결 방법 3: Next.js API Routes (BFF 패턴) 활용하기
Next.js를 사용하는 풀스택/프론트엔드 환경이라면 프레임워크 자체의 API 라우트를 일종의 중간 서버(BFF, Backend For Frontend)로 사용할 수 있습니다.
브라우저 ➔ Next.js 서버(서버 사이드) ➔ 외부 백엔드 API 순서로 통신하면, 서버 간 통신에는 브라우저의 CORS 제약이 적용되지 않으므로 아주 안전하게 데이터를 가져올 수 있습니다.
// app/api/proxy-user/route.ts (Next.js App Router API Route)
import { NextResponse } from 'next/server';
export async function GET() {
// 서버 사이드에서 외부 API 호출 (CORS 제약 없음)
const response = await fetch('http://localhost:5000/api/users');
const data = await response.json();
return NextResponse.json(data);
}

5. 결론 및 느낀 점
CORS 에러는 처음에 만나면 당황스럽고 막막하지만, "브라우저가 사용자를 악의적인 크로스 사이트 요청(CSRF/XSS 등)으로부터 보호하기 위해 작동하는 정상적인 보안 방패"라는 점을 이해하면 대처 방법이 명확해집니다.
- 개발 환경에서는 프론트엔드 Proxy 설정으로 빠르게 개발하고,
- 배포 환경에서는 백엔드 서버의 CORS 미들웨어 및 Nginx 리버스 프록시로 명확한 도메인을 허용해 주는 것이 가장 이상적입니다.
이제 브라우저 콘솔창에 빨간색 CORS 에러가 떠도 당황하지 마시고, 프록시 설정이나 서버 헤더부터 침착하게 점검해 보세요!
'개발' 카테고리의 다른 글
| [Git/GitHub] 커밋했는데 잔디가 안 심어진다? 5분 만에 해결한 Git 이메일 연동 삽질기 (0) | 2026.08.15 |
|---|---|
| [React] useEffect의 올바른 사용법과 무한 리렌더링(Infinite Loop) 탈출기 (0) | 2026.08.13 |
| [VS Code] 프론트엔드 개발 생산성을 2배로 올려주는 필수 확장 프로그램 5선 (0) | 2026.08.11 |
| [TypeScript] "빨간 줄과의 전쟁" 초보 개발자가 자주 범하는 타입 에러 3가지와 해결법 (0) | 2026.08.08 |
| [JS] for문만 돌리던 내가 map, filter, reduce를 자유자재로 쓰게 된 계기 (0) | 2026.08.05 |