[FastAPI - CORS 마스터 3부] 개발(Local) vs 운영(Prod) 환경 분리와 실전 트러블 슈팅
지난 1부와 2부를 거치며 우리는 CORS의 개념을 익히고, 인증 정보가 포함된 요청까지 안전하게 처리하는 방법을 구현했습니다. 로컬 개발 환경에서 프론트엔드와 백엔드는 이제 완벽하게 소통하고 있습니다. 기능 개발이 완료되었으니, 이제 코드를 실제 서버에 배포할 차례입니다.
하지만 여기서 우리는 마지막 난관에 봉착하게 됩니다. 2부에서 작성한 코드를 다시 한번 살펴보겠습니다. 소스 코드 내부에 http://localhost:3000이라는 로컬 주소가 하드코딩 되어 있습니다. 이 코드가 그대로 프로덕션 서버(AWS, GCP 등)에 올라간다면 어떻게 될까요? 실제 사용자는 https://my-service.com 같은 도메인을 통해 접속할 텐데, 서버는 여전히 로컬호스트만 허용하고 있으니 당연히 모든 요청이 차단될 것입니다.
그렇다고 배포할 때마다 소스 코드를 수정해서 git에 올리는 것은 매우 위험하고 비효율적인 방식입니다. 개발 환경(Local), 테스트 환경(Stage), 운영 환경(Production)은 각각 다른 허용 출처(Allow Origins)를 가져야 하며, 이 설정은 코드 바깥에서 주입되어야 합니다. 이것이 바로 The Twelve-Factor App 원칙 중 하나인 설정(Config)의 분리입니다.
FastAPI 생태계에서는 pydantic-settings 라이브러리를 통해 이 문제를 아주 우아하게 해결할 수 있습니다. .env 파일이나 환경 변수를 읽어와서 타입 안전성(Type Safety)을 보장받으며 설정을 관리하는 방식입니다.
저는 보통 다음과 같이 config.py를 구성하여 CORS 설정을 관리합니다.
# config.py
from pydantic_settings import BaseSettings
from typing import List
class Settings(BaseSettings):
# 환경 변수에서 BACKEND_CORS_ORIGINS 값을 읽어옵니다.
# JSON 문자열 리스트(예: ["http://localhost:3000", "https://my-domain.com"])로 파싱됩니다.
BACKEND_CORS_ORIGINS: List[str] = []
class Config:
case_sensitive = True
env_file = ".env"
settings = Settings()
그리고 메인 애플리케이션 코드(main.py)는 더 이상 특정 도메인을 알 필요가 없도록 추상화합니다.
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from config import settings
app = FastAPI()
# 설정 파일에서 로드한 리스트를 그대로 적용
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
이제 환경별로 .env 파일만 다르게 두면 됩니다. 로컬 개발자의 컴퓨터에는 localhost가 적힌 .env 파일이, 실제 운영 서버에는 서비스 도메인이 적힌 환경 변수가 주입되므로 코드는 수정할 필요 없이 어디서든 완벽하게 동작합니다.
환경 분리가 끝났다고 해서 안심하기는 이릅니다. 제가 실제 운영 환경에 배포하고 나서 겪었던, 정말 사소하지만 치명적인 실수 두 가지를 공유하고자 합니다. 이 내용은 CORS 에러 로그만으로는 원인을 파악하기 힘든 경험적인 트러블 슈팅입니다.
첫 번째는 프로토콜 불일치 문제입니다.
배포 후 도메인을 연결하면 보통 HTTPS(SSL)를 적용하게 됩니다. 프론트엔드는 https://my-service.com에서 실행됩니다. 하지만 개발자가 환경 변수 설정 시 실수로 http://my-service.com (S가 빠짐)이라고 적거나, 혹은 그 반대의 경우가 빈번하게 발생합니다.
브라우저의 SOP 정책은 프로토콜(http vs https)까지 엄격하게 구분합니다. 서버의 허용 목록에 있는 문자열과 요청을 보낸 출처(Origin) 헤더의 문자열이 글자 하나라도 다르면 브라우저는 가차 없이 차단합니다. 배포 직후 CORS 에러가 뜬다면 가장 먼저 주소창의 프로토콜과 환경 변수 설정값이 정확히 일치하는지 확인해야 합니다.
두 번째는 후행 슬래시(Trailing Slash)와 리다이렉트 문제입니다.
이것은 FastAPI와 Starlette 프레임워크의 특성을 모르면 정말 해결하기 어려운 문제입니다. 예를 들어, 백엔드에 @app.post("/users")라는 엔드포인트를 만들었다고 가정해 봅시다. 그런데 프론트엔드 코드에서 실수로 요청 URL을 /users/ (끝에 슬래시 포함)로 보냈다면 어떤 일이 벌어질까요?
FastAPI는 기본적으로 후행 슬래시가 붙은 요청이 들어오면, 슬래시를 제거한 URL(/users)로 307 Temporary Redirect 응답을 보냅니다. 브라우저는 이 307 응답을 받고 자동으로 올바른 주소로 재요청을 시도합니다.
문제는 이 과정에서 발생합니다. 브라우저가 첫 번째 요청(슬래시 포함)을 보냈을 때 Preflight(OPTIONS) 요청이 발생할 수 있는데, 리다이렉트 응답에는 CORS 헤더가 제대로 포함되지 않는 경우가 생깁니다. 또는 리다이렉트 된 주소로 다시 요청할 때 Origin 헤더가 null로 바뀌어 버리는 브라우저 이슈가 발생하기도 합니다.
결과적으로 개발자 도구에는 영문 모를 CORS 에러가 찍히게 되는데, 근본적인 원인은 CORS 설정이 아니라 URL 뒤에 붙은 슬래시 하나 때문인 것입니다. 따라서 API를 호출할 때는 서버에 정의된 URL과 완전히 동일한 경로(슬래시 유무 포함)를 사용하는 것이 정신 건강에 좋습니다.
CORS는 웹 개발을 시작하는 사람들이 가장 먼저 마주하는 벽이자, 가장 늦게까지 괴롭히는 문제입니다. 하지만 그 원리를 이해하고 보면, 이는 우리를 괴롭히려는 것이 아니라 사용자를 보호하기 위한 브라우저의 충실한 방어 기제임을 알 수 있습니다. 1부의 기초 설정, 2부의 인증 처리, 그리고 이번 3부의 환경 분리와 트러블 슈팅까지의 내용을 통해 여러분의 FastAPI 서버가 어떤 환경에서도 안전하고 유연하게 동작하기를 바랍니다.
아래는 환경 변수를 통해 개발 환경과 운영 환경의 설정이 어떻게 분리되어 주입되는지, 그리고 배포 과정에서의 흐름을 시각화한 것입니다.

'ServerDev > FastAPI' 카테고리의 다른 글
| [FastAPI] 미들웨어(Middleware) - 개념부터 커스텀 구현까지 (0) | 2025.12.12 |
|---|---|
| [FastAPI] BackgroundTasks 심층 분석: 0.1초 응답의 비밀과 치명적 한계 (vs Celery, Custom Loop) (0) | 2025.12.12 |
| [FastAPI - CORS 마스터 2부] 쿠키 인증(Credential) 이슈 해결과 보안을 위한 정교한 허용 전략 (0) | 2025.12.11 |
| FastAPI - CORS 마스터 1부] 프론트엔드 연동 첫걸음, CORS 에러 (0) | 2025.12.10 |
| [FastAPI] 웹 프레임워크가 아니라 'AI 모델 서빙기'로 사용하기 (feat. Spring Boot) (0) | 2025.12.09 |