개발/FastAPI

FastAPI CORS 설정 방법: 프론트엔드 연동할 때 막히는 이유

notebase 2026. 5. 29. 16:17

React, Vue, Next.js 같은 프론트엔드에서 FastAPI API 호출이 CORS 오류로 막히는 이유와 CORSMiddleware 설정 방법을 초보자 기준으로 정리합니다.

 

FastAPI CORS 설정은 프론트엔드에서 백엔드 API를 호출할 때 자주 막히는 지점이다. React, Vue, Next.js 개발 서버와 FastAPI 서버를 따로 실행하면 브라우저 콘솔에 CORS 오류가 뜨는 경우가 많다.

처음 보면 FastAPI 코드가 잘못된 것처럼 보인다.

하지만 실제로는 API 로직 문제가 아니라 브라우저 보안 정책 때문에 요청이 차단되는 상황인 경우가 많다.


 

CORS가 뭔가요?

CORS는 Cross-Origin Resource Sharing의 줄임말이다.

한국어로 풀면 “교차 출처 리소스 공유” 정도로 번역할 수 있다. 말은 어렵지만, 핵심은 단순하다.

 

다른 출처에서 온 요청을 서버가 허용할지 정하는 방식

 

예를 들어 프론트엔드는 아래 주소에서 실행 중이라고 해보자.

http://localhost:5173

 

FastAPI 서버는 아래 주소에서 실행 중이다.

http://localhost:8000

 

둘 다 내 컴퓨터에서 실행 중이지만 브라우저는 이 둘을 같은 곳으로 보지 않는다. 포트 번호가 다르기 때문이다.

이 상태에서 프론트엔드가 FastAPI 서버로 요청을 보내면 브라우저는 먼저 확인한다.

 

http://localhost:5173에서 온 요청이 http://localhost:8000 서버에 접근해도 되나요?

 

FastAPI 서버가 “허용한다”고 알려주면 응답을 프론트엔드 코드에서 사용할 수 있다.

반대로 서버가 허용 정보를 주지 않으면 브라우저가 응답을 막고 CORS 오류를 보여준다.

여기서 중요한 점은 CORS가 서버가 죽어서 나는 오류가 아니라는 것이다. 서버는 정상 응답을 했는데, 브라우저가 그 응답을 프론트엔드 코드에 넘기지 않는 경우도 있다.

그래서 CORS 설정은 단순히 오류를 없애는 코드가 아니다.

어떤 프론트엔드 주소가 내 FastAPI 서버에 접근할 수 있는지 정하는 설정이다.


 

CORS 오류는 왜 발생할까?

웹에서는 다음 세 가지가 모두 같아야 같은 출처, 즉 같은 Origin으로 본다.

프로토콜: http 또는 https
도메인: localhost, example.com 등
포트: 5173, 8000 등

 

아래 두 주소를 비교해보자.

http://localhost:5173
http://localhost:8000

 

프로토콜은 http로 같다.

도메인도 localhost로 같다.

하지만 포트가 다르다.

프론트엔드: http://localhost:5173
백엔드:     http://localhost:8000

 

그래서 브라우저는 이 둘을 서로 다른 Origin으로 본다.

프론트엔드 개발 서버와 백엔드 API 서버를 분리해서 실행하면 CORS 오류를 자주 만나는 이유가 여기에 있다.


 

FastAPI에서 CORS 설정하는 기본 방법

FastAPI에서는 CORSMiddleware를 추가해서 CORS를 설정한다.

FastAPI 공식 문서에서도 CORS 설정은 CORSMiddleware를 임포트하고, 허용할 Origin 목록을 만든 뒤, FastAPI 애플리케이션에 미들웨어로 추가하는 방식으로 안내한다.

기본 코드는 아래와 같다.

# main.py

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

app = FastAPI()

origins = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
)


@app.get("/")
def read_root():
    return {"message": "Hello FastAPI"}

 

이 설정에서 가장 중요한 부분은 allow_origins다.

origins = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]

 

여기에 프론트엔드가 실행되는 주소를 넣어야 한다.

Vite 기반 React 프로젝트라면 보통 아래 주소를 많이 쓴다.

http://localhost:5173

 

Create React App이나 Next.js 개발 서버는 기본적으로 아래 주소를 많이 쓴다.

http://localhost:3000

 

다만 프로젝트 설정에 따라 포트는 달라질 수 있다. 브라우저 주소창에 실제로 표시되는 프론트엔드 주소를 확인한 뒤 넣는 게 가장 정확하다.


 

각 옵션은 무슨 의미일까?

CORS 설정 코드를 처음 보면 옵션이 많아서 헷갈릴 수 있다.

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
)

 

각 옵션의 의미는 아래와 같다.

옵션 의미
allow_origins 요청을 허용할 프론트엔드 주소
allow_credentials 쿠키, 인증 헤더 등 인증 정보를 포함할 수 있는지
allow_methods 허용할 HTTP 메서드
allow_headers 허용할 요청 헤더

 

개발 중에는 아래처럼 넓게 허용하는 예시도 자주 보인다.

allow_methods=["*"]
allow_headers=["*"]

 

다만 allow_credentials=True를 함께 사용할 때는 주의해야 한다.

FastAPI 공식 문서 기준으로 allow_credentials=True인 경우 allow_origins, allow_methods, allow_headers["*"]로 설정할 수 없다. 모두 명시적으로 지정해야 한다.

그래서 쿠키나 인증 정보를 포함하는 요청을 다룬다면 아래처럼 실제 값을 적어주는 편이 좋다.

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",
        "https://example.com",
    ],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
)

 

입문 단계에서는 이 부분을 놓치기 쉽다. 단순 조회 API만 테스트할 때는 문제가 없어 보이다가, 로그인이나 인증 요청을 붙이는 순간 CORS 오류가 다시 나타날 수 있다.


 

프론트엔드 요청 예시

프론트엔드에서는 보통 fetchaxios로 FastAPI API를 호출한다.

fetch("http://localhost:8000/")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  });

 

FastAPI에 CORS 설정이 없거나, allow_origins에 현재 프론트엔드 주소가 빠져 있으면 브라우저 콘솔에 대략 이런 오류가 뜰 수 있다.

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

 

이 메시지에서 봐야 할 부분은 두 가지다.

fetch at 'http://localhost:8000/'
origin 'http://localhost:5173'

 

즉, http://localhost:5173에서 http://localhost:8000으로 요청했는데 허용되지 않았다는 뜻이다.

이때 FastAPI 설정의 allow_originshttp://localhost:5173을 추가해야 한다.


 

localhost127.0.0.1은 다르게 취급될 수 있다

입문자가 자주 놓치는 부분이 있다.

아래 두 주소는 비슷해 보인다.

http://localhost:5173
http://127.0.0.1:5173

 

하지만 Origin 기준에서는 문자열이 다르다.

프론트엔드를 http://127.0.0.1:5173으로 열어놓고, FastAPI CORS 설정에는 http://localhost:5173만 넣으면 CORS 오류가 날 수 있다.

그래서 개발 환경에서는 둘 다 넣어두는 편이 안전하다.

origins = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]

 

실제로는 프론트엔드를 어떤 주소로 접속했는지 브라우저 주소창을 먼저 확인해야 한다.


 

임시로 모든 Origin을 허용해도 될까?

개발 중에는 아래처럼 모든 Origin을 허용하는 예시를 볼 수 있다.

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

 

이렇게 하면 여러 프론트엔드 주소에서 접근할 수 있어서 테스트가 편하다.

하지만 이 설정을 그대로 운영 환경에 가져가는 것은 조심해야 한다.

특히 로그인, 쿠키, 인증 토큰처럼 사용자 정보가 오가는 API라면 더 신중해야 한다.

그리고 allow_credentials=True를 사용한다면 아래처럼 와일드카드와 섞어서 쓰지 않는 편이 좋다.

# 피해야 할 예시
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

 

FastAPI 공식 문서에서도 credentials를 허용하는 경우에는 allow_origins, allow_methods, allow_headers를 모두 명시적으로 지정해야 한다고 안내한다.

운영 환경에서는 보통 실제 서비스 도메인만 허용한다.

origins = [
    "https://example.com",
    "https://www.example.com",
]

 

개발 환경과 운영 환경을 나눠서 설정하는 방식이 더 안전하다.


 

쿠키나 인증 정보를 보낼 때는 더 주의해야 한다

프론트엔드에서 쿠키 기반 로그인이나 인증 헤더를 사용한다면 allow_credentials=True가 필요할 수 있다.

예를 들어 axios에서 쿠키를 포함해 요청하려면 프론트엔드 코드에도 설정이 들어간다.

axios.get("http://localhost:8000/me", {
  withCredentials: true,
});

 

FastAPI 쪽에서는 아래처럼 설정한다.

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",
    ],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
)

 

여기서 오해하기 쉬운 부분이 있다.

인증 정보가 오가는 요청이라면 어떤 프론트엔드 도메인에서 접근할 수 있는지 명확히 제한해야 한다.

운영 환경에서 allow_origins=["*"]처럼 모든 출처를 열어두는 방식은 피하는 편이 좋다. 특히 allow_credentials=True와 함께 쓸 때는 allow_methods, allow_headers까지 실제 필요한 값으로 명시하는 것이 공식 문서 기준에 맞다.


 

Preflight 요청 때문에 막히는 경우도 있다

CORS 오류를 보다 보면 실제 요청 전에 OPTIONS 요청이 먼저 나가는 경우가 있다.

이걸 Preflight 요청이라고 한다.

브라우저가 본 요청을 보내기 전에 서버에 먼저 확인하는 과정이다.

 

이 Origin에서 이런 메서드와 헤더로 요청해도 되나요?

 

예를 들어 Authorization 헤더를 붙이거나, Content-Type: application/json으로 POST 요청을 보내는 경우 Preflight 요청이 발생할 수 있다.

MDN 문서에서도 Preflight 요청은 실제 요청 전에 브라우저가 OPTIONS 메서드로 서버에 확인하는 요청이라고 설명한다.

FastAPI에서는 CORSMiddleware를 올바르게 추가하면 이런 Preflight 요청도 처리된다.

즉, 보통은 개발자가 OPTIONS 라우터를 직접 만들 필요가 없다.


 

CORS 설정했는데도 안 될 때 확인할 것

CORS 설정을 했는데도 계속 오류가 난다면 아래 순서로 확인해보면 좋다.

1. 프론트엔드 주소가 정확한가?

브라우저 주소창을 확인한다.

http://localhost:5173
http://127.0.0.1:5173
http://localhost:3000

 

이 주소가 allow_origins에 정확히 들어가 있어야 한다.

끝에 /를 붙여서 아래처럼 쓰면 의도와 다르게 동작할 수 있다.

# 권장하지 않음
"http://localhost:5173/"

 

보통은 아래처럼 쓴다.

"http://localhost:5173"

 


2. FastAPI 서버를 재시작했는가?

CORS 설정을 바꿨는데 서버가 이전 코드로 계속 실행 중일 수 있다.

개발 서버를 재시작하거나, --reload 옵션을 사용 중인지 확인한다.

fastapi dev main.py

 

또는 uvicorn을 직접 실행한다면 아래처럼 실행할 수 있다.

uvicorn main:app --reload

 


3. 요청 URL이 맞는가?

프론트엔드에서 요청하는 주소가 실제 FastAPI 서버 주소와 다를 수 있다.

fetch("http://localhost:8000/api/items")

 

FastAPI 서버가 8000번 포트에서 실행 중인지, API 경로가 /api/items가 맞는지 확인해야 한다.

CORS 오류처럼 보이지만 실제로는 경로 오류, 프록시 설정 오류, 서버 실행 오류가 섞여 있는 경우도 있다.


4. 프론트엔드 프록시 설정과 충돌하지 않는가?

Vite나 Next.js에서 프록시를 설정해두면 요청 흐름이 달라진다.

예를 들어 프론트엔드에서 아래처럼 요청한다고 해보자.

fetch("/api/items")

 

이 경우 브라우저가 바로 http://localhost:8000으로 요청하는 것이 아니라, 프론트엔드 개발 서버의 프록시 설정을 거칠 수 있다.

프록시를 사용할지, FastAPI에 직접 요청할지 먼저 정해야 한다.

둘을 섞어두면 오류 원인을 찾기 어렵다.


5. 커스텀 헤더를 보내고 있지 않은가?

프론트엔드에서 직접 만든 헤더를 보내는 경우도 있다.

fetch("http://localhost:8000/items", {
  headers: {
    "X-Client-Version": "1.0.0",
  },
});

 

이런 헤더를 사용한다면 FastAPI의 allow_headers에도 해당 헤더 이름을 추가해야 한다.

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=[
        "Authorization",
        "Content-Type",
        "X-Client-Version",
    ],
)

 

Authorization, Content-Type 외에 프로젝트에서 직접 사용하는 헤더가 있다면 이 목록에 빠지지 않았는지 확인해야 한다.

예를 들어 사내 프로젝트나 토이 프로젝트에서 X-API-Key, X-Client-Id 같은 헤더를 직접 붙여 쓰는 경우가 있다. 이때도 같은 방식으로 allow_headers에 추가하면 된다.


 

개발 환경과 운영 환경 설정을 나누는 예시

실제 프로젝트에서는 환경 변수로 허용 Origin을 관리하는 방식이 좋다.

간단한 예시는 아래와 같다.

# main.py

import os

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

app = FastAPI()

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

if ENV == "production":
    origins = [
        "https://example.com",
        "https://www.example.com",
    ]
else:
    origins = [
        "http://localhost:5173",
        "http://127.0.0.1:5173",
        "http://localhost:3000",
        "http://127.0.0.1:3000",
    ]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
)


@app.get("/health")
def health_check():
    return {"status": "ok"}

 

이렇게 해두면 개발 중에는 로컬 프론트엔드 주소를 허용하고, 운영 환경에서는 실제 배포 도메인만 허용할 수 있다.

만약 프로젝트에서 커스텀 헤더를 사용한다면 이 예시의 allow_headers에도 해당 헤더를 추가하면 된다.

allow_headers=[
    "Authorization",
    "Content-Type",
    "X-Client-Version",
]

 

물론 실제 서비스에서는 환경 변수를 더 체계적으로 관리하는 편이 좋다. 예를 들어 .env 파일이나 배포 플랫폼의 환경 변수 설정을 사용할 수 있다.


 

FastAPI CORS 설정 전체 예시

입문자라면 일단 아래 코드로 시작해도 충분하다.

# main.py

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

app = FastAPI()

origins = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
    "http://localhost:3000",
    "http://127.0.0.1:3000",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=[
        "Authorization",
        "Content-Type",
        # 커스텀 헤더를 사용한다면 여기에 추가
        # "X-Client-Version",
    ],
)


@app.get("/")
def read_root():
    return {
        "message": "FastAPI CORS 설정 완료"
    }


@app.get("/items")
def read_items():
    return [
        {"id": 1, "name": "Apple"},
        {"id": 2, "name": "Banana"},
    ]

 

프론트엔드에서는 이렇게 호출할 수 있다.

fetch("http://localhost:8000/items")
  .then((response) => response.json())
  .then((data) => console.log(data));

 

브라우저 콘솔에 데이터가 정상 출력되면 기본 연동은 성공이다.


 

만약 인증이 없는 단순 API라면?

로그인, 쿠키, 인증 헤더가 전혀 없는 단순 테스트 API라면 개발 중에는 아래처럼 간단히 열어둘 수도 있다.

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

 

다만 이 설정은 “테스트 편의용”에 가깝다.

나중에 로그인 기능이나 인증 헤더를 붙일 예정이라면 처음부터 명시적인 Origin 방식으로 작성하는 편이 덜 헷갈린다.


 

CORS는 에러를 없애는 설정이 아니라 허용 범위를 정하는 설정이다

CORS를 단순히 “오류 해결용 코드”로만 보면 나중에 운영 환경에서 설정을 과하게 열어둘 수 있다.

실제로는 이 질문에 답하는 설정에 가깝다.

 

어떤 프론트엔드 주소가 이 API 서버에 접근할 수 있는가?

 

개발 중에는 localhost127.0.0.1을 넉넉히 허용해도 된다.

하지만 배포할 때는 실제 서비스 도메인만 남기는 편이 좋다.

FastAPI에서 프론트엔드 연동이 막힌다면 먼저 브라우저 콘솔의 CORS 메시지를 보고, 요청을 보낸 Origin과 allow_origins 값을 비교해보면 된다. 대부분의 로컬 개발 CORS 오류는 여기서 원인이 나온다.

특히 인증 요청이 포함된다면 allow_credentials=True와 와일드카드 설정을 함께 쓰고 있지 않은지도 같이 확인해야 한다.

그리고 직접 만든 헤더를 프론트엔드에서 보내고 있다면 allow_headers에 해당 헤더 이름이 포함되어 있는지도 확인하자.