Express + React CORS 에러 해결 방법 (2026 가이드)

2026년 3월 게시 • 읽기 시간 8분

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 에러 디버거 열기 →

관련 개발자 도구