본문 바로가기

CORS 설정과 미들웨어 구현

1. CORS란

CORS(Cross-Origin Resource Sharing)는 웹 브라우저에서 보안상의 이유로 다른 출처(Origin)의 리소스에 접근하는 것을 제한하는 정책입니다. 예를 들어, http://localhost:5500에서 실행되는 프론트엔드가 http://localhost:8000에서 실행되는 FastAPI 서버에 요청을 보내면 CORS 에러가 발생합니다.

1.1 출처가 같다는 것

먼저 '출처(Origin)'가 무엇인지 정확히 알아야 합니다. 출처는 아래 세 가지가 모두 같아야 같은 것으로 취급됩니다.

구성 요소예시
프로토콜http 또는 https
호스트localhost, weniv.co.kr
포트5500, 8000, 생략 시 80 또는 443

http://localhost:5500과 http://localhost:8000을 비교해보면 프로토콜과 호스트는 같지만 포트가 다릅니다. 사람 눈에는 같은 내 컴퓨터로 보이지만 브라우저는 다른 출처로 판단합니다.

http://weniv.co.kr과 https://weniv.co.kr도 다른 출처입니다. 프로토콜이 다르기 때문입니다.

1.2 브라우저가 요청을 막는 순서

어떠한 프로세스를 거쳐 CORS가 가능한지 알아보겠습니다.

  1. 브라우저: "hey, example.com에 요청을 보내려고 하는데..."

    • Origin: myapp.com
    • 요청 메서드: POST
    • 요청 헤더: Content-Type: application/json
  2. 브라우저가 자동으로 "preflight" 요청을 보냄 (OPTIONS 메서드)

    • "이런 요청을 보내도 될까요?"
  3. 서버가 응답

    • Access-Control-Allow-Origin: myapp.com
    • Access-Control-Allow-Methods: POST
    • Access-Control-Allow-Headers: Content-Type
    • "네, 그런 요청을 보내도 좋습니다!"
  4. 브라우저: "서버가 허용한다고 하니, 이제 실제 요청을 보내겠습니다" (실제 POST 요청 전송)

여기서 중요한 포인트는 브라우저가 차단을 먼저 하는 것이 아니라 서버에 "허가"를 먼저 요청합니다. 서버의 CORS 설정은 이 "허가"에 대한 응답입니다. 허가하지 않는 응답을 받으면 그때 브라우저가 차단하는 것이죠.

서버에는 요청이 도착했는데 브라우저에서 에러가 납니다

CORS를 처음 만나면 가장 헷갈리는 지점입니다. 터미널에는 요청이 들어온 로그가 찍히는데 브라우저 콘솔에는 빨간 에러가 뜹니다.

CORS는 브라우저가 지키는 규칙이기 때문입니다. 서버는 요청을 받고 응답까지 정상적으로 보냈습니다. 브라우저가 그 응답에 허가 표시가 없다고 보고 JavaScript에게 전달하지 않는 것입니다.

그래서 .http 파일이나 curl로는 CORS 에러가 나지 않습니다. 브라우저가 아니기 때문입니다. 이 성질을 알아두면 "API는 되는데 화면에서만 안 된다"는 상황에서 원인을 빨리 찾을 수 있습니다.

이러한 CORS 에러를 해결하기 위해서는 서버에서 특정 출처의 요청을 허용하도록 설정해야 합니다. FastAPI에서는 CORSMiddleware를 통해 별도의 라이브러리 설치 없이 이를 쉽게 구현할 수 있습니다.

2. CORS 미들웨어 설정

2.1 미들웨어

미들웨어는 모든 요청과 응답이 반드시 거쳐 가는 통로입니다.

엔드포인트마다 같은 처리를 반복하지 않아도 되는 것이 장점입니다. CORS 허용 헤더를 붙이는 일, 요청 처리 시간을 기록하는 일, 접속 로그를 남기는 일 등이 미들웨어로 처리됩니다.

2.2 설정 코드

05_blog 폴더에 main.py 파일을 만들고 다음과 같이 CORS 미들웨어를 설정합니다.

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

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 수업에서는 모든 출처를 허용합니다
    allow_credentials=False,
    allow_methods=["*"],  # 모든 HTTP 메서드 허용
    allow_headers=["*"],  # 모든 HTTP 헤더 허용
)


@app.get("/api/hello")
async def read_root():
    return {"message": "안녕하세요!"}

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

옵션의미
allow_origins허용할 출처 목록입니다. ["*"]는 전부 허용입니다
allow_credentials쿠키를 주고받을지 여부입니다
allow_methods허용할 HTTP 메서드입니다
allow_headers허용할 요청 헤더입니다

allow_origins=["*"]와 allow_credentials=True는 함께 쓸 수 없습니다

두 값을 동시에 켜면 브라우저가 요청을 거부합니다. 표준에서 금지하고 있기 때문입니다. "모든 사이트를 허용하면서 쿠키까지 주고받겠다"는 것은 아무 사이트나 내 쿠키를 쓸 수 있게 하겠다는 뜻이라 위험합니다.

쿠키가 필요하다면 출처를 명시적으로 적어야 합니다.

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

우리 실습은 토큰을 헤더로 주고받으므로 쿠키가 필요 없습니다. 그래서 allow_credentials=False로 두었습니다.

실무에서는 *를 쓰지 마세요

allow_origins=["*"]는 "어떤 사이트에서든 우리 API를 호출해도 좋다"는 뜻입니다. 실습에서는 편하지만, 실제 서비스에서는 우리 도메인만 적어야 합니다.

allow_origins=[
    "https://weniv.co.kr",
    "https://www.weniv.co.kr",
]

환경에 따라 이 목록을 바꾸는 방법은 7장 설정 관리에서 다룹니다.

3. 간단한 예제

이제 프론트엔드와 백엔드를 연동하는 간단한 예제를 만들어보겠습니다.

3.1 백엔드

위의 main.py를 그대로 사용합니다. 서버를 실행합니다.

fastapi dev

3.2 프론트엔드

index.html 파일을 만들고 아래 내용을 작성합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>CORS 테스트</title>
</head>
<body>
    <h1>CORS 테스트</h1>
    <button onclick="fetchMessage()">메시지 가져오기</button>
    <p id="message"></p>

    <script>
        async function fetchMessage() {
            try {
                const response = await fetch('http://127.0.0.1:8000/api/hello');
                const data = await response.json();
                document.getElementById('message').textContent = data.message;
            } catch (error) {
                console.error('Error:', error);
                document.getElementById('message').textContent = '요청 실패, 콘솔을 확인하세요';
            }
        }
    </script>
</body>
</html>

4. 실행 방법

  1. 백엔드 서버 실행

    fastapi dev
    
  2. 프론트엔드 실행

    • VS Code의 Live Server 확장을 사용하여 index.html을 실행합니다. 파일에서 마우스 우클릭 후 Open with Live Server를 선택하면 됩니다.
    • 기본적으로 http://127.0.0.1:5500에서 실행됩니다.

버튼을 클릭하면 백엔드 서버로부터 메시지를 가져와 화면에 표시합니다. CORS가 정상적으로 설정되었다면 오류 없이 메시지가 표시될 것입니다.

4.1 막히는 것도 확인해보기

CORS 설정이 실제로 일하고 있다는 것을 확인해보겠습니다. main.py에서 미들웨어 부분을 주석 처리하고 서버를 다시 실행하세요.

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

버튼을 다시 눌러보면 화면에는 아무것도 나오지 않고, 개발자 도구 콘솔에 아래와 비슷한 에러가 나타납니다.

Access to fetch at 'http://127.0.0.1:8000/api/hello' from origin
'http://127.0.0.1:5500' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

이때 FastAPI 터미널을 확인해보세요. GET /api/hello 200 OK 로그가 찍혀 있습니다. 서버는 정상적으로 응답했고, 브라우저가 그 응답을 JavaScript에게 넘기지 않은 것입니다.

확인이 끝나면 주석을 다시 해제해주세요.

5. 미들웨어 직접 만들어보기

CORS 외에도 미들웨어를 직접 만들 수 있습니다. 요청이 얼마나 걸렸는지 재는 미들웨어를 만들어보겠습니다.

import time

from fastapi import FastAPI, Request

app = FastAPI()


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.perf_counter()
    response = await call_next(request)  # 여기서 실제 엔드포인트가 실행됩니다
    process_time = time.perf_counter() - start_time
    response.headers["X-Process-Time"] = f"{process_time:.4f}"
    print(f"{request.method} {request.url.path} - {process_time:.4f}초")
    return response

call_next(request)를 기준으로 위는 요청이 들어올 때, 아래는 응답이 나갈 때 실행됩니다. 서버를 실행하고 아무 요청이나 보내보면 터미널에 처리 시간이 찍히고, 응답 헤더에 X-Process-Time이 추가된 것을 확인할 수 있습니다.

.http 파일에서 응답을 보면 헤더 영역에서 확인할 수 있습니다.

이제 기본적인 CORS 설정이 완료되었습니다. 다음 절에서는 이를 바탕으로 실제 블로그 기능을 구현해보도록 하겠습니다.

연습문제

  1. allow_methods=["GET"]으로 바꾸고 POST 요청을 보내보세요. 어떤 에러가 나오는지 확인해보세요.
  2. allow_origins=["http://127.0.0.1:5500"]으로 바꾸고, Live Server 포트를 다른 값으로 바꿔서 실행해보세요.
  3. 요청이 0.5초 이상 걸리면 터미널에 경고를 출력하는 미들웨어를 만들어보세요.
CORS 설정과 미들웨어 구현 - FastAPI 베이스캠프 | 위니버시티