CORS와 Proxy

프론트엔드-백엔드 통신의 벽과 우회 전략

브라우저의 Same-Origin Policy가 API 호출을 차단하는 원리를 설명한다. CORS 헤더로 허용하는 방법, 개발 환경에서 Vite Proxy로 우회하는 방법, 프로덕션 배포 시 Reverse Proxy 구성까지 단계별로 정리한다.

Engineering
저자

Kwangmin Kim

공개

2026년 05월 05일

1 배경: 왜 두 개의 서버가 필요한가

FastAPI 입문에서 Python으로 API 서버를 만들었다. 이 서버는 localhost:8000에서 JSON 데이터를 주고받는다. 그런데 실제 서비스에서는 사용자가 브라우저에서 보는 화면(프론트엔드)이 필요하다. 이 화면을 만드는 기술이 React, Vue 같은 프론트엔드 프레임워크이다.

개발 환경에서는 이 구조가 두 개의 별도 서버로 나뉜다:

프론트엔드 개발 서버 (localhost:5173)  ←  화면(HTML/CSS/JS)을 제공
백엔드 API 서버   (localhost:8000)  ←  데이터(JSON)를 제공

브라우저는 프론트엔드 서버에서 화면을 받아 표시하고, 화면의 JavaScript 코드가 백엔드 서버에 데이터를 요청한다. 이때 포트가 다르기 때문에 브라우저가 요청을 차단하는 문제가 발생한다. 이것이 이 포스트에서 다루는 CORS 문제이다.

  • CORS(브라우저 관점): 브라우저가 다른 출처(origin: 프로토콜+호스트+포트)로부터 온 응답을 JavaScript가 읽는 것을 기본적으로 차단하는 보안 정책이다. 서버가 Access-Control-Allow-* 헤더로 허용을 선언하면 브라우저가 응답을 허용한다.
  • 왜 필요한가: 악성 사이트가 사용자의 인증 쿠키를 이용해 백엔드에 임의 요청을 보내는 것을 막기 위함이다.
  • FastAPI에서의 해결(서버 쪽): CORSMiddleware로 허용 출처와 메서드를 설정하면 브라우저의 Preflight(OPTIONS) 요청에 응답해준다.
# Python (FastAPI)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
  CORSMiddleware,
  allow_origins=["http://localhost:5173"],
  allow_methods=["*"],
  allow_headers=["*"],
  allow_credentials=True,
)
  • 프록시(개념): 클라이언트의 요청을 대신 전달해 주는 중간 서버. 개발환경에서는 프론트엔드 개발 서버가 백엔드로 요청을 프록시하면 브라우저 입장에선 같은 출처로 보이므로 CORS가 문제되지 않는다. 프로덕션에서는 Nginx/Caddy 같은 리버스 프록시가 프론트엔드 정적 파일과 API를 같은 도메인으로 묶어준다.
  • Vite 개발 프록시 예시:
// vite.config.ts
export default defineConfig({
  server: {
    port: 5173,
    proxy: {
      "/agents": { target: "http://localhost:8000", changeOrigin: true }
    }
  }
});
  • 차이점 요약:
    • CORS = 서버가 브라우저에게 허용을 ’선언’하는 방식(브라우저 기반 보안 제어).
    • Proxy = 요청을 서버-서버로 전달해 브라우저가 다른 출처에 직접 접근하지 않게 하는 우회(동작 위치: 프론트/리버스 서버).
  • 실무 권장:
    • 개발: Vite Proxy로 편리하게 개발하고, 동시에 백엔드엔 안전한 CORS 설정을 해두기.
    • 프로덕션: 리버스 프록시(Nginx 등)로 같은 도메인에서 서빙하거나, 필요하면 엄격한 CORS 정책 사용.
  • 주의사항: allow_origins=["*"]allow_credentials=True는 브라우저에서 허용되지 않음; 자격증명(쿠키/인증) 사용 시 특정 출처만 지정해야 함.
용어: fetch

fetch브라우저에 내장된 HTTP 클라이언트 함수이다. Python의 requests.get()이나 터미널의 curl과 같은 역할을 하지만, 브라우저의 JavaScript 코드 안에서 실행된다.

// 브라우저 JavaScript에서 백엔드 API를 호출하는 코드
const response = await fetch("http://localhost:8000/agents/qna_chatbot/run", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ text: "RAG란?" })
});
const data = await response.json();

참고) const는 블록 스코프 상수 선언자다 — 선언한 식별자에 재할당할 수 없지만, 객체나 배열 같은 값의 내부는 변경할 수 있다.

const x = 1;
x = 2; // TypeError: Assignment to constant variable.

const obj = { a: 1 };
obj.a = 2; // 허용
obj = {};  // TypeError

fetch 호출이 CORS 차단의 대상이 된다. curl은 브라우저가 아니므로 차단되지 않는다.

2 문제: 왜 API 호출이 차단되는가

2.1 용어 정리

브라우저 (localhost:5173)
↓ fetch("/agents/…")
Vite 개발 서버 (localhost:5173) — 프록시로 전달
↓ proxy -> http://localhost:8000
FastAPI (localhost:8000) — API 응답(JSON)

(브라우저는 5173에만 요청하므로 CORS 없음)
  • 클라이언트(client): 최종 사용자 쪽 프로그램(예: 브라우저, 모바일 앱). 사용자가 직접 실행하고 UI를 보여준다.
  • 프론트엔드(frontend): 클라이언트에서 실행되는 코드·자산(HTML/CSS/JS, React 등). 화면 렌더링과 사용자 입력 처리를 담당한다.
  • 서버(server): 네트워크로 요청을 듣고 응답을 주는 쪽의 컴퓨터(또는 프로세스). 예: uvicorn, nginx가 서버 역할을 한다.
  • 백엔드(backend): 서버에서 동작하는 애플리케이션 로직(예: FastAPI), 데이터베이스 접근, 인증, 외부 API 호출 등을 수행한다.

관계(흐름): - 사용자가 클라이언트(브라우저)에서 UI(프론트엔드)를 조작 → 프론트엔드는 HTTP 요청(fetch/ajax)으로 백엔드(서버)를 호출 → 백엔드는 비즈니스 로직 수행 + DB 접근 → 응답(JSON/HTML)을 프론트엔드에 반환 → 프론트엔드가 화면 업데이트.
- 물리적/운영적 분리: 프론트엔드와 백엔드는 같은 머신에 있을 수도 있고(프로덕션: 리버스 프록시로 통합), 개발 중엔 서로 다른 포트/서버로 분리되어 실행된다(예: localhost:5173 프론트엔드, localhost:8000 백엔드).
- 중간 요소: 리버스 프록시(Nginx), CDN, 인증 게이트웨이 등은 요청을 라우팅·보호한다.
- 보안/브라우저 제약: 브라우저는 출처(origin)를 기준으로 요청을 제한(CORS); 프록시를 쓰면 브라우저 입장에서는 같은 출처로 보여 CORS 문제를 피할 수 있다.

프론트엔드 개발 서버(localhost:5173)에서 받은 JavaScript 코드가 FastAPI 서버(localhost:8000)로 fetch를 보내면 브라우저가 요청을 차단한다.

Access to fetch at 'http://localhost:8000/agents/qna_chatbot/run'
from origin 'http://localhost:5173' has been blocked by CORS policy

서버는 정상이고, curl로 같은 요청을 보내면 문제없이 응답한다. 차단의 주체는 서버가 아니라 브라우저이다. 이 브라우저 보안 정책이 Same-Origin Policy이다.

3 Same-Origin Policy

정의: Origin (출처)

Origin은 프로토콜 + 호스트 + 포트의 조합이다.

URL Origin
http://localhost:5173/home http://localhost:5173
http://localhost:8000/api http://localhost:8000
https://example.com/page https://example.com (포트 443 암묵)
  • Same-Origin Policy는 출처가 다른 리소스에 대한 JavaScript 접근을 차단하는 브라우저 보안 정책이다.
  • 포트가 하나만 달라도 다른 출처로 간주한다.
비교 같은 출처? 이유
http://localhost:5173 vs http://localhost:8000 X 포트 다름
http://example.com vs https://example.com X 프로토콜 다름
http://api.example.com vs http://example.com X 호스트 다름
http://localhost:5173/a vs http://localhost:5173/b O 경로만 다름

3.1 왜 이런 정책이 필요한가

Same-Origin Policy가 없으면 악성 사이트가 사용자의 브라우저를 통해 다른 사이트의 API를 호출할 수 있다.

1. 사용자가 은행 사이트에 로그인한 상태
2. 악성 사이트(evil.com)를 방문
3. evil.com의 JavaScript가 bank.com/api/transfer를 호출
4. 브라우저가 bank.com 쿠키를 자동으로 첨부
5. 사용자 모르게 송금 실행

Same-Origin Policy는 3번 단계에서 요청을 차단하여 이 시나리오를 방지한다.

3.2 차단 대상과 비차단 대상

모든 크로스 오리진 요청이 차단되는 것은 아니다.

요소 차단 여부 이유
<img src="..."> 허용 이미지 로드는 데이터 접근 없음
<script src="..."> 허용 CDN에서 라이브러리 로드 허용
<link href="..."> 허용 외부 CSS 로드
fetch() / XMLHttpRequest 차단 JavaScript가 응답 데이터를 읽을 수 있으므로

fetch가 차단되는 이유는 JavaScript 코드가 응답 본문을 읽고 조작할 수 있기 때문이다. 이미지 태그는 픽셀만 렌더링하고 데이터를 코드로 읽을 수 없으므로 허용된다.

4 CORS: 서버가 허용을 선언하는 방식

정의: CORS (Cross-Origin Resource Sharing)

CORS는 서버가 HTTP 응답 헤더를 통해 “이 출처의 요청은 허용한다”고 브라우저에 알리는 메커니즘이다. Same-Origin Policy의 예외를 안전하게 허용하는 표준이다.

4.1 단순 요청 (Simple Request)

조건을 충족하는 요청은 Preflight 없이 바로 전송된다.

  • 메서드: GET, HEAD, POST 중 하나
  • 헤더: Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나
  • 커스텀 헤더 없음
[브라우저] → GET /data HTTP/1.1
             Origin: http://localhost:5173

[서버]    ← HTTP/1.1 200 OK
             Access-Control-Allow-Origin: http://localhost:5173

서버 응답에 Access-Control-Allow-Origin 헤더가 있고 요청 출처와 일치하면, 브라우저가 응답을 JavaScript에 전달한다.

4.2 Preflight 요청

JSON 본문(Content-Type: application/json)을 보내는 POST 요청처럼 단순 요청 조건을 벗어나면, 브라우저가 먼저 OPTIONS 요청을 보내 서버에 허용 여부를 확인한다.

[브라우저] → OPTIONS /agents/qna_chatbot/run HTTP/1.1
             Origin: http://localhost:5173
             Access-Control-Request-Method: POST
             Access-Control-Request-Headers: Content-Type

[서버]    ← HTTP/1.1 200 OK
             Access-Control-Allow-Origin: http://localhost:5173
             Access-Control-Allow-Methods: POST, GET, OPTIONS
             Access-Control-Allow-Headers: Content-Type

[브라우저] → POST /agents/qna_chatbot/run HTTP/1.1   ← 본 요청
             Origin: http://localhost:5173
             Content-Type: application/json
             {"text": "RAG란?"}

[서버]    ← HTTP/1.1 200 OK
             Access-Control-Allow-Origin: http://localhost:5173
             {"response": {"text": "..."}}

AI Agent API에서 JSON 본문을 사용하므로 거의 모든 POST 요청은 Preflight를 거친다. 이 OPTIONS 요청은 브라우저가 자동으로 보내며, 개발자가 명시적으로 작성하지 않는다.

4.3 CORS 관련 헤더 정리

헤더 방향 설명
Origin 요청 요청을 보낸 출처 (브라우저가 자동 추가)
Access-Control-Allow-Origin 응답 허용할 출처 (* 또는 특정 출처)
Access-Control-Allow-Methods 응답 허용할 HTTP 메서드 목록
Access-Control-Allow-Headers 응답 허용할 요청 헤더 목록
Access-Control-Allow-Credentials 응답 쿠키/인증 정보 포함 허용 여부
Access-Control-Max-Age 응답 Preflight 결과 캐시 시간 (초)

5 FastAPI에서 CORS 설정

FastAPI는 CORSMiddleware로 CORS 헤더를 자동 추가한다.

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

이 설정이 하는 일:

  • 모든 요청에 Access-Control-Allow-Origin 헤더를 추가한다
  • OPTIONS Preflight 요청에 자동으로 200 응답을 보낸다
  • allow_credentials=True이면 Access-Control-Allow-Credentials: true를 추가한다

5.1 주요 매개변수

매개변수 설명 개발용 프로덕션용
allow_origins 허용할 출처 목록 ["http://localhost:5173"] ["https://app.example.com"]
allow_methods 허용할 HTTP 메서드 ["*"] ["GET", "POST"]
allow_headers 허용할 요청 헤더 ["*"] ["Content-Type", "Authorization"]
allow_credentials 쿠키 포함 허용 True 필요할 때만 True
경고

allow_origins=["*"]allow_credentials=True는 동시에 사용할 수 없다. 브라우저가 거부한다. 자격 증명을 포함하는 요청은 반드시 특정 출처를 명시해야 한다.

5.2 환경별 분리

import os

ENVIRONMENT = os.getenv("ENVIRONMENT", "development")

if ENVIRONMENT == "development":
    origins = ["http://localhost:5173", "http://localhost:3000"]
else:
    origins = ["https://app.example.com"]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

개발 환경에서는 여러 로컬 포트를 허용하고, 프로덕션에서는 실제 도메인만 허용한다.

6 개발 환경: Vite Proxy

CORS 설정은 서버 측 해결이다. 개발 환경에서는 프론트엔드 프록시로 Same-Origin Policy 자체를 우회할 수 있다.

용어: Vite

Vite는 프론트엔드 개발 서버 + 빌드 도구이다. React, Vue 같은 프론트엔드 프레임워크로 만든 코드를 브라우저에서 실행할 수 있도록 변환하고, 개발 중에는 localhost:5173에서 파일 변경을 감지하여 즉시 반영한다. FastAPI에서 uvicorn이 하는 역할의 프론트엔드 버전이다.

Vite의 핵심 기능 중 하나가 프록시 설정으로, 프론트엔드에서 백엔드로의 API 요청을 대신 전달해주어 CORS 문제를 우회할 수 있다.

6.1 원리

프록시는 프론트엔드 개발 서버가 API 요청을 대신 전달하는 방식이다. 브라우저 입장에서는 같은 출처(localhost:5173)에 요청하므로 CORS가 발생하지 않는다.

프록시 없이:
브라우저(5173) → fetch("http://localhost:8000/agents/...") → CORS 차단

프록시 사용:
브라우저(5173) → fetch("/agents/...") → Vite(5173) → FastAPI(8000) → 응답
                  같은 출처이므로 CORS 없음        서버 간 통신이므로 CORS 없음

핵심은 브라우저는 localhost:5173에만 요청하고, 5173 → 8000 전달은 서버 간 통신이므로 Same-Origin Policy가 적용되지 않는다는 것이다.

6.2 Vite 설정

Vite의 설정 파일(vite.config.ts)에서 프록시 규칙을 정의한다. 이 파일은 프론트엔드 프로젝트 루트에 위치한다.

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    proxy: {
      "/agents": {
        target: "http://localhost:8000",
        changeOrigin: true,
      },
      "/monitoring": {
        target: "http://localhost:8000",
        changeOrigin: true,
      },
      "/health": {
        target: "http://localhost:8000",
        changeOrigin: true,
      },
    },
  },
});

/agents로 시작하는 모든 요청은 Vite가 http://localhost:8000/agents로 전달한다. 프론트엔드 코드에서는 상대 경로만 사용한다.

// 프록시 사용 시 — 상대 경로
const response = await fetch("/agents/qna_chatbot/run", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ text: "RAG란?" }),
});

// 프록시 미사용 시 — 절대 경로 (CORS 필요)
const response = await fetch("http://localhost:8000/agents/qna_chatbot/run", {
  // ...
});

6.3 Proxy vs CORS: 어떤 것을 사용할까

기준 Vite Proxy CORS 미들웨어
동작 위치 프론트엔드 개발 서버 백엔드 서버
적용 범위 개발 환경만 개발 + 프로덕션
설정 대상 vite.config.ts FastAPI main.py
브라우저 네트워크 탭 같은 출처로 보임 크로스 오리진으로 보임
Preflight 오버헤드 없음 있음 (OPTIONS 요청)

개발 환경에서는 둘 다 설정하는 것이 일반적이다. Proxy가 주된 해결책이고, CORS는 프록시를 거치지 않는 직접 호출(테스트, Swagger UI 등)을 위한 백업이다.

7 프로덕션: Reverse Proxy

개발 환경의 Vite Proxy는 빌드 후 사라진다. 프로덕션에서는 Reverse Proxy(Nginx, Caddy, 클라우드 로드 밸런서 등)가 같은 역할을 한다.

개발:
브라우저 → Vite(5173) → FastAPI(8000)

프로덕션:
브라우저 → Nginx(443) ──┬── /api/* → FastAPI(8000)
                        └── /*     → React 정적 파일

Nginx 설정 예시:

server {
    listen 443 ssl;
    server_name app.example.com;

    # React 정적 파일
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    # API 프록시
    location /agents/ {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /monitoring/ {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
    }
}

프론트엔드와 백엔드가 같은 도메인, 같은 포트로 서빙되므로 CORS 자체가 발생하지 않는다. 이것이 프로덕션에서 가장 깔끔한 해결책이다.

7.1 Docker 멀티스테이지 빌드와의 결합

Docker에서 React와 FastAPI를 하나의 컨테이너로 배포할 때도 같은 원리가 적용된다.

# Stage 1: React 빌드
FROM node:20 AS frontend
WORKDIR /app/frontend
COPY frontend/ .
RUN npm ci && npm run build

# Stage 2: FastAPI + 정적 파일 서빙
FROM python:3.12
COPY --from=frontend /app/frontend/dist /app/static
COPY src/ /app/src/

# FastAPI가 정적 파일도 함께 서빙
CMD ["uvicorn", "src.services.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
from fastapi.staticfiles import StaticFiles

# React 빌드 결과를 FastAPI가 직접 서빙
app.mount("/", StaticFiles(directory="/app/static", html=True), name="static")

하나의 서버가 프론트엔드와 API를 모두 서빙하므로 Same-Origin이 된다. 소규모 프로젝트에서는 별도 Nginx 없이 이 방식으로 충분하다.

8 디버깅 가이드

8.1 CORS 에러 확인 방법

브라우저 개발자 도구에서 확인할 수 있다.

1. F12 → Network 탭
2. 실패한 요청 선택
3. Response Headers에서 Access-Control-Allow-Origin 확인
4. Console 탭에서 CORS 에러 메시지 확인

8.2 흔한 실수와 해결

증상 원인 해결
No 'Access-Control-Allow-Origin' header 서버에 CORS 미들웨어 미설정 CORSMiddleware 추가
Preflight OPTIONS 405 서버가 OPTIONS 메서드 미처리 allow_methods=["*"] 설정
쿠키가 전송되지 않음 credentials 설정 누락 allow_credentials=True + fetchcredentials: "include"
프록시 설정했는데 CORS 발생 절대 경로 사용 상대 경로(/agents/...)로 변경
개발에선 되는데 프로덕션에서 안 됨 Vite Proxy가 빌드 후 사라짐 Nginx Reverse Proxy 또는 CORS 헤더 설정

8.3 curl로 CORS 시뮬레이션

curl은 브라우저가 아니므로 CORS가 적용되지 않지만, Origin 헤더를 수동으로 추가하여 서버 응답을 확인할 수 있다.

# Preflight 시뮬레이션
curl -X OPTIONS http://localhost:8000/agents/qna_chatbot/run \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -v

# 응답에 Access-Control-Allow-Origin 헤더가 있는지 확인

9 관련 주제

선행 지식

  • API 기초 – HTTP, JSON, 상태 코드
  • FastAPI 입문 – 8단계에서 CORS 미들웨어 설정을 다룸. 이 포스트는 그 원리를 심화한다

후속 주제

Subscribe

Enjoy this blog? Get notified of new posts by email: