프론트엔드 개발을 처음 배우고 백엔드 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 미들웨어를 설치해 간단히 해결할 수 있습니다.

JavaScript
 
// 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 검열을 완벽히 우회할 수 있습니다.

JSON
 
// package.json (Create React App 기준)
{
  "name": "my-frontend-app",
  "version": "0.1.0",
  "proxy": "http://localhost:5000"
}

또는 Vite를 사용 중이라면 vite.config.ts 파일에서 아래와 같이 깔끔하게 설정할 수 있습니다.

TypeScript
 
// 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 제약이 적용되지 않으므로 아주 안전하게 데이터를 가져올 수 있습니다.

TypeScript
 
// 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);
}

200 OK 성공 응답 화면


5. 결론 및 느낀 점

CORS 에러는 처음에 만나면 당황스럽고 막막하지만, "브라우저가 사용자를 악의적인 크로스 사이트 요청(CSRF/XSS 등)으로부터 보호하기 위해 작동하는 정상적인 보안 방패"라는 점을 이해하면 대처 방법이 명확해집니다.

  • 개발 환경에서는 프론트엔드 Proxy 설정으로 빠르게 개발하고,
  • 배포 환경에서는 백엔드 서버의 CORS 미들웨어 및 Nginx 리버스 프록시로 명확한 도메인을 허용해 주는 것이 가장 이상적입니다.

이제 브라우저 콘솔창에 빨간색 CORS 에러가 떠도 당황하지 마시고, 프록시 설정이나 서버 헤더부터 침착하게 점검해 보세요!

+ Recent posts