CORS 에러란?
Express.js 백엔드와 통신하는 React 프론트엔드를 구축했다면, 브라우저 콘솔에서 이 에러를 거의 확실히 본 적이 있을 것입니다:
Access to fetch at 'http://localhost:3001/api/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
CORS는 Cross-Origin Resource Sharing(교차 출처 리소스 공유)의 약자입니다. 하나의 출처(도메인 + 포트 + 프로토콜)에서 다른 출처로의 HTTP 요청을 제한하는 브라우저 보안 메커니즘입니다. 이 정책은 서버가 아닌 브라우저에서 시행합니다. 요청이 출처를 넘을 때, 브라우저는 먼저 서버가 허용하는지 확인하기 위해 특정 HTTP 응답 헤더, 주로 Access-Control-Allow-Origin을 확인합니다.
해당 헤더가 누락되거나 잘못된 경우, 서버가 200 OK를 반환하더라도 브라우저는 응답을 차단하고 CORS 에러를 발생시킵니다. 이것은 Express 버그가 아닙니다. 브라우저가 정확히 해야 할 일을 하는 것입니다.
React + Express에서 CORS가 발생하는 이유
일반적인 React + Express 개발 환경에서는 두 개의 별도 서버를 실행합니다:
- React 개발 서버:
http://localhost:3000(Create React App) 또는http://localhost:5173(Vite) - Express API 서버:
http://localhost:3001(또는 다른 포트)
포트가 다르기 때문에 브라우저는 이들을 다른 출처로 취급합니다. React 앱에서 Express API로의 모든 fetch 또는 axios 호출은 교차 출처 요청이며, Express 서버가 명시적으로 허용하지 않는 한 브라우저가 차단합니다.
프로덕션에서는 문제가 사라지는 경우가 많습니다 — 같은 도메인에 배포하면(예: Next.js에서 둘 다 서빙하거나 Nginx 리버스 프록시로 /api를 Express로 라우팅) — 하지만 개발 중에 두 서버를 별도로 실행하면 CORS 에러가 거의 확실히 발생합니다.
빠른 해결: cors 미들웨어 설치
Express에서 CORS를 해결하는 가장 빠른 방법은 공식 cors npm 패키지를 사용하는 것입니다. 설치합니다:
npm install cors
그런 다음 라우트 정의 전에 Express 앱에 추가합니다:
// server.js (또는 app.js)
const express = require('express');
const cors = require('cors');
const app = express();
// 모든 출처 허용 — 개발 환경에서만 사용
app.use(cors());
app.use(express.json());
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello from Express!' });
});
app.listen(3001, () => {
console.log('Server running on http://localhost:3001');
});
인자 없이 app.use(cors())를 호출하면 모든 응답에 Access-Control-Allow-Origin: *를 추가하여 모든 출처의 요청을 허용합니다. 로컬 개발에서는 괜찮지만 인증된 데이터를 처리하는 API의 프로덕션에서는 사용해서는 안 됩니다.
프로덕션 설정
프로덕션에서는 항상 허용되는 출처를 정확히 지정하고, 자격 증명, 메서드, 헤더를 명시적으로 설정하십시오:
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://www.yourdomain.com',
'https://yourdomain.com',
'http://localhost:3000', // 로컬 개발용 유지
'http://localhost:5173', // Vite 개발 서버
];
// 출처가 없는 요청 허용 (예: curl, Postman, 서버 간 통신)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS blocked for origin: ${origin}`));
}
},
credentials: true, // 쿠키와 Authorization 헤더 허용
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['Content-Range', 'X-Content-Range'],
maxAge: 86400, // 프리플라이트 응답을 24시간 캐싱
};
app.use(cors(corsOptions));
// 모든 라우트에 대해 프리플라이트 요청을 명시적으로 처리
app.options('*', cors(corsOptions));
이 설정에 대한 몇 가지 핵심 사항:
credentials: true— React 앱이 쿠키나Authorization헤더를 보내는 경우 필수입니다. 이 옵션이 설정되면origin은*가 될 수 없으며 특정 출처여야 합니다.app.options('*', cors())— 브라우저는 단순하지 않은 요청(PUT, PATCH, DELETE 또는 커스텀 헤더가 있는 요청) 전에 HTTP OPTIONS 프리플라이트 요청을 보냅니다. Express가 이에 응답해야 합니다. 그렇지 않으면 실제 요청이 차단됩니다.maxAge— 브라우저에 프리플라이트 응답을 얼마나 오래 캐싱할지 알려주어 불필요한 OPTIONS 요청을 줄입니다.
개발 환경용 React 프록시 대안
Create React App을 사용하는 경우, package.json에 proxy 필드를 추가하여 개발 중 CORS를 완전히 우회할 수 있습니다. 이렇게 하면 webpack 개발 서버가 API 요청을 Express 백엔드로 전달합니다:
// package.json (React 프로젝트 루트)
{
"name": "my-react-app",
"version": "1.0.0",
"proxy": "http://localhost:3001",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
}
}
이제 React 코드에서 fetch('/api/data')(호스트명 없음에 주의)를 호출하면, 개발 서버가 요청을 http://localhost:3001/api/data로 프록시합니다 — 동일 출처이므로 CORS가 발생하지 않습니다. 이 방법은 개발에서만 작동하며 프로덕션에서는 배포 환경에서 라우팅을 처리해야 합니다.
Vite 프록시 설정
Vite(Create React App의 최신 대안)를 사용하는 경우, vite.config.js에서 프록시를 설정합니다:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
// /api로 시작하는 모든 요청을 Express로 전달
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
secure: false,
// 선택 사항: Express 라우트가 /api로 시작하지 않는 경우 경로 재작성
// rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
이 설정으로 React 컴포넌트에서 fetch('/api/users')를 호출하면 http://localhost:3001/api/users로 프록시됩니다. 개발 중에는 CORS 헤더가 필요하지 않습니다. Express 서버가 깔끔하게 유지됩니다.
일반적인 실수
1. 와일드카드 origin과 credentials 함께 사용
이 조합은 항상 실패합니다. 브라우저는 Access-Control-Allow-Origin: *와 Access-Control-Allow-Credentials: true가 함께 있는 CORS 응답을 거부합니다:
// 이것은 작동하지 않습니다 — 브라우저가 와일드카드 + credentials를 거부합니다
app.use(cors({
origin: '*', // 와일드카드
credentials: true, // 이 둘을 결합할 수 없습니다
}));
해결 방법: '*'를 위의 프로덕션 설정에 표시된 것처럼 특정 출처 문자열이나 출처를 검증하는 함수로 대체하십시오.
2. 프리플라이트용 OPTIONS 핸들러 누락
Express 앱이 OPTIONS 요청을 처리하지 않으면, PUT, PATCH, DELETE 및 커스텀 헤더 요청에 대한 프리플라이트 검사가 404를 수신하고 실제 요청은 절대 전송되지 않습니다. 라우트 정의 전에 항상 app.options('*', cors(corsOptions))를 추가하십시오.
3. CORS 헤더를 수동으로 설정하면서 cors 패키지도 사용
Access-Control-Allow-Origin 헤더를 이중으로 설정하면 브라우저가 중복 값을 보고 요청을 차단합니다. 하나의 방법 — cors 미들웨어 또는 수동 헤더 — 을 선택하고 그것만 사용하십시오. 절대 둘 다 사용하지 마십시오.
4. CORS 미들웨어를 라우트 뒤에 추가
Express의 미들웨어는 등록된 순서대로 실행됩니다. 라우트 핸들러 뒤에 app.use(cors())를 추가하면, 라우트가 먼저 매칭되고 CORS 헤더가 추가되지 않습니다. 항상 CORS 미들웨어를 미들웨어 스택의 맨 위에 배치하십시오.
CORS 에러 디버깅
CORS 에러가 발생하면, 브라우저 콘솔 메시지는 어떤 헤더가 누락되었거나 일치하지 않는지 알려줍니다. DevTools의 Network 탭에서 Express 서버가 반환한 실제 응답 헤더를 확인하고 예상되는 것과 비교하십시오.
확인해야 할 일반적인 사항:
- 응답에
Access-Control-Allow-Origin이 있습니까? - 헤더의 origin이 React 앱의 출처(프로토콜과 포트 포함)와 정확히 일치합니까?
- 자격 증명이 포함된 요청:
Access-Control-Allow-Credentials: true가 있습니까? - PUT/DELETE 요청: OPTIONS 프리플라이트가
Access-Control-Allow-Methods와 함께 200을 반환했습니까? - 커스텀 헤더(예:
Authorization)를 보내고 있습니까?Access-Control-Allow-Headers에 포함되어야 합니다.
CORS 에러를 즉시 디버깅하세요
CORS 에러 메시지를 붙여넣고 Express, Nginx, Apache 등에 대한 정확한 수정 코드를 얻으세요 — 추측이 필요 없습니다.
CORS 에러 디버거 열기 →관련 개발자 도구
- CORS 에러 디버거 — 에러를 붙여넣고 Express, Nginx, Apache 등의 수정 코드 확인
- JWT 디코더 — Authorization 헤더와 JWT 페이로드 검사
- HTTP 상태 코드 — 404, 403, 401, 500 등 모든 상태 코드 이해
- ENV 검사기 — Express 앱의 환경 변수 검증
- 모든 무료 개발자 도구 보기