커스텀 응답 - HTML, 스트림, 파일 등

커스텀 응답 - HTML, 스트림, 파일 등 (Custom Response - HTML, Stream, File, others)

기본적으로 FastAPI는 JSON 응답을 돌려줘요.

Return a Response directly에서 본 것처럼 Response를 직접 돌려주면 이 기본 동작을 바꿀 수 있어요.

하지만 Response를 직접 돌려주면(JSONResponse 같은 하위 클래스도 마찬가지), 데이터가 자동으로 변환되지 않고(response_model을 선언해도), 문서도 자동으로 생성되지 않아요. 예를 들어 생성된 OpenAPI에 특정 "미디어 타입"을 HTTP 헤더 Content-Type에 포함시키는 일 같은 거죠.

하지만 path operation 데코레이터에서 response_class 파라미터를 사용해, 사용하길 원하는 Response(예: 어떤 Response 하위 클래스든)를 선언할 수도 있어요.

path operation 함수에서 돌려주는 내용이 그 Response 안에 담겨요.

참고

미디어 타입이 없는 응답 클래스를 사용하면, FastAPI는 응답에 내용이 없다고 기대해요. 그래서 생성된 OpenAPI 문서에 응답 형식을 문서화하지 않아요.

출처: 공식문서

JSON 응답

기본적으로 FastAPI는 JSON 응답을 돌려줘요.

Response Model을 선언하면 FastAPI가 Pydantic을 사용해서 데이터를 JSON으로 직렬화해요.

응답 모델을 선언하지 않으면, FastAPI는 JSON Compatible Encoder에서 설명한 jsonable_encoder를 사용하고 그 결과를 JSONResponse에 넣어요.

JSONResponse처럼 JSON 미디어 타입(application/json)을 가진 response_class를 선언하면, path operation 데코레이터에서 선언한 Pydantic response_model이 있으면 돌려주는 데이터가 자동으로 변환(그리고 필터링)돼요. 하지만 Pydantic으로 JSON 바이트로 직렬화되지는 않아요. 대신 jsonable_encoder로 변환된 다음 JSONResponse 클래스로 전달되고, 이 클래스가 파이썬 표준 JSON 라이브러리를 사용해 바이트로 직렬화해요.

JSON 성능

요약하면, 최대 성능을 원한다면 Response Model을 사용하고 path operation 데코레이터response_class를 선언하지 마세요.

Python 3.10+

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []


@app.post("/items/")
async def create_item(item: Item) -> Item:
    return item


@app.get("/items/")
async def read_items() -> list[Item]:
    return [
        Item(name="Portal Gun", price=42.0),
        Item(name="Plumbus", price=32.0),
    ]

HTML 응답

FastAPI에서 HTML을 담은 응답을 직접 돌려주려면 HTMLResponse를 사용해요.

  • HTMLResponse를 임포트하세요.
  • path operation 데코레이터response_class 파라미터로 HTMLResponse를 전달하세요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.get("/items/", response_class=HTMLResponse)
async def read_items():
    return """
    <html>
        <head>
            <title>Some HTML in here</title>
        </head>
        <body>
            <h1>Look ma! HTML!</h1>
        </body>
    </html>
    """

참고

response_class 파라미터는 응답의 "미디어 타입"을 정의하는 데도 사용돼요.

이 경우 HTTP 헤더 Content-Typetext/html로 설정돼요.

그리고 OpenAPI에서도 그렇게 문서화돼요.

Response 돌려주기

Return a Response directly에서 본 것처럼, path operation 안에서 Response를 돌려주어 직접 바꿀 수도 있어요.

위의 예시와 같은 것을 HTMLResponse를 돌려주는 방식으로 바꾸면 이렇게 될 수 있어요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.get("/items/")
async def read_items():
    html_content = """
    <html>
        <head>
            <title>Some HTML in here</title>
        </head>
        <body>
            <h1>Look ma! HTML!</h1>
        </body>
    </html>
    """
    return HTMLResponse(content=html_content, status_code=200)

경고

path operation 함수가 직접 돌려주는 Response는 OpenAPI에 문서화되지 않고(예: Content-Type이 문서화되지 않음) 자동 대화형 문서에도 보이지 않아요.

참고

물론 실제 Content-Type 헤더, 상태 코드 같은 것들은 여러분이 돌려준 Response 객체에서 나와요.

OpenAPI에 문서화하면서 Response 바꾸기

함수 안에서 응답을 바꾸면서 동시에 "미디어 타입"을 OpenAPI에 문서화하고 싶다면, response_class 파라미터를 사용하면서 Response 객체를 돌려주면 돼요.

그러면 response_class는 OpenAPI path operation을 문서화하는 데만 사용되고, 여러분의 Response는 그대로 사용돼요.

HTMLResponse 직접 돌려주기

예를 들어 이렇게 될 수 있어요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()


def generate_html_response():
    html_content = """
    <html>
        <head>
            <title>Some HTML in here</title>
        </head>
        <body>
            <h1>Look ma! HTML!</h1>
        </body>
    </html>
    """
    return HTMLResponse(content=html_content, status_code=200)


@app.get("/items/", response_class=HTMLResponse)
async def read_items():
    return generate_html_response()

이 예시에서 generate_html_response() 함수는 이미 Response를 생성해서 돌려주고 있어요. HTML을 str로 돌려주는 대신에요.

generate_html_response() 호출 결과를 돌려줌으로써, 이미 기본 FastAPI 동작을 바꿀 Response를 돌려주고 있는 거예요.

하지만 response_class에도 HTMLResponse를 전달했기 때문에, FastAPI는 OpenAPI와 대화형 문서에서 이를 text/html의 HTML로 문서화하는 방법을 알게 돼요.

사용 가능한 응답들

여기 사용 가능한 응답 몇 가지를 소개할게요.

Response를 사용해 다른 어떤 것이든 돌려줄 수 있고, 심지어 커스텀 하위 클래스를 만들 수도 있다는 점을 기억하세요.

기술적 세부 사항

from starlette.responses import HTMLResponse를 쓸 수도 있어요.

FastAPIstarlette.responsesfastapi.responses로 동일하게 제공하는 건 개발자(여러분)를 위한 편의일 뿐이에요. 대부분의 응답은 실제로 Starlette에서 바로 온 거예요.

Response

주요 Response 클래스예요. 다른 모든 응답들이 여기서 상속받아요.

직접 돌려줄 수 있어요.

다음 파라미터들을 받아요.

  • content - str 또는 bytes.
  • status_code - int HTTP 상태 코드.
  • headers - 문자열들의 dict.
  • media_type - 미디어 타입을 주는 str. 예: "text/html".

FastAPI(사실 Starlette)가 자동으로 Content-Length 헤더를 포함해요. 미디어 타입에 기반한 Content-Type 헤더도 포함하는데, 텍스트 타입에는 charset을 덧붙여요.

Python 3.10+

from fastapi import FastAPI, Response

app = FastAPI()


@app.get("/legacy/")
def get_legacy_data():
    data = """<?xml version="1.0"?>
    <shampoo>
    <Header>
        Apply shampoo here.
    </Header>
    <Body>
        You'll have to use soap here.
    </Body>
    </shampoo>
    """
    return Response(content=data, media_type="application/xml")

HTMLResponse

일부 텍스트나 바이트를 받아 HTML 응답을 돌려줘요. 위에서 읽은 대로요.

PlainTextResponse

일부 텍스트나 바이트를 받아 일반 텍스트(plain text) 응답을 돌려줘요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import PlainTextResponse

app = FastAPI()


@app.get("/", response_class=PlainTextResponse)
async def main():
    return "Hello World"

JSONResponse

일부 데이터를 받아 application/json으로 인코딩된 응답을 돌려줘요.

이게 FastAPI에서 사용되는 기본 응답이에요. 위에서 읽은 대로요.

기술적 세부 사항

하지만 응답 모델이나 반환 타입을 선언하면, 그 값이 데이터를 JSON으로 직렬화하는 데 직접 사용되고, JSON에 맞는 미디어 타입을 가진 응답이 JSONResponse 클래스를 사용하지 않고 직접 돌려져요.

최고의 성능을 얻는 이상적인 방법이에요.

RedirectResponse

HTTP 리다이렉트를 돌려줘요. 기본적으로 307 상태 코드(Temporary Redirect)를 사용해요.

RedirectResponse를 직접 돌려줄 수 있어요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/typer")
async def redirect_typer():
    return RedirectResponse("https://typer.tiangolo.com")

또는 response_class 파라미터에서 사용할 수 있어요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/fastapi", response_class=RedirectResponse)
async def redirect_fastapi():
    return "https://fastapi.tiangolo.com"

이렇게 하면 path operation 함수에서 URL을 직접 돌려줄 수 있어요.

이 경우 사용되는 status_codeRedirectResponse의 기본값인 307이에요.


status_code 파라미터를 response_class 파라미터와 함께 사용할 수도 있어요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/pydantic", response_class=RedirectResponse, status_code=302)
async def redirect_pydantic():
    return "https://docs.pydantic.dev/"

StreamingResponse

비동기 제너레이터(generator)나 일반 제너레이터/이터레이터(yield가 있는 함수)를 받아 응답 본문을 스트리밍해요.

Python 3.10+

import anyio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()


async def fake_video_streamer():
    for i in range(10):
        yield b"some fake video bytes"
        await anyio.sleep(0)


@app.get("/")
async def main():
    return StreamingResponse(fake_video_streamer())

기술적 세부 사항

async 태스크는 await에 도달했을 때만 취소될 수 있어요. await가 없으면 제너레이터(yield가 있는 함수)는 제대로 취소될 수 없고, 취소가 요청된 후에도 계속 실행될 수 있어요.

이 작은 예시는 await 문이 필요 없으므로, 이벤트 루프가 취소를 처리할 기회를 주기 위해 await anyio.sleep(0)을 추가해요.

이건 크거나 무한한 스트림에서 훨씬 중요해져요.

StreamingResponse를 직접 돌려주는 대신, Stream Data의 스타일을 따르는 게 좋아요. 훨씬 편리하고 취소도 뒤에서 처리해 주니까요.

JSON Lines를 스트리밍한다면 Stream JSON Lines 튜토리얼을 따라가세요.

FileResponse

파일을 응답으로 비동기로 스트리밍해요.

다른 응답 타입들과는 다른 인자 집합을 받아 생성돼요.

  • path - 스트리밍할 파일의 경로.
  • headers - 포함할 커스텀 헤더들. 딕셔너리 형태.
  • media_type - 미디어 타입을 주는 문자열. 설정하지 않으면 파일 이름이나 경로로 미디어 타입을 추론해요.
  • filename - 설정하면 응답의 Content-Disposition에 포함돼요.

파일 응답은 적절한 Content-Length, Last-Modified, ETag 헤더를 포함해요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import FileResponse

some_file_path = "large-video-file.mp4"
app = FastAPI()


@app.get("/")
async def main():
    return FileResponse(some_file_path)

response_class 파라미터로도 사용할 수 있어요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import FileResponse

some_file_path = "large-video-file.mp4"
app = FastAPI()


@app.get("/", response_class=FileResponse)
async def main():
    return some_file_path

이 경우 path operation 함수에서 파일 경로를 직접 돌려줄 수 있어요.

커스텀 응답 클래스 만들기

Response에서 상속받아 여러분만의 커스텀 응답 클래스를 만들고 사용할 수 있어요.

예를 들어 orjson을 어떤 설정과 함께 사용하고 싶다고 해볼게요.

들여쓰기되고 형식화된 JSON을 돌려주고 싶어서, orjson.OPT_INDENT_2라는 orjson 옵션을 사용하고 싶다고 해봐요.

CustomORJSONResponse를 만들 수 있어요. 핵심은 내용을 bytes로 돌려주는 Response.render(content) 메서드를 만드는 거예요.

Python 3.10+

from typing import Any

import orjson
from fastapi import FastAPI, Response

app = FastAPI()


class CustomORJSONResponse(Response):
    media_type = "application/json"

    def render(self, content: Any) -> bytes:
        assert orjson is not None, "orjson must be installed"
        return orjson.dumps(content, option=orjson.OPT_INDENT_2)


@app.get("/", response_class=CustomORJSONResponse)
async def main():
    return {"message": "Hello World"}

이제 이렇게 돌려주는 대신,

{"message": "Hello World"}

...이 응답은 이렇게 돌려줘요.

{
  "message": "Hello World"
}

물론 JSON을 형식화하는 것보다 훨씬 더 좋은 활용 방법을 찾게 될 거예요. 😉

orjson 또는 Response Model

성능을 찾고 있다면 orjson 응답보다 Response Model을 사용하는 게 낫습니다.

응답 모델을 사용하면 FastAPI가 Pydantic으로 데이터를 JSON으로 직렬화해요. jsonable_encoder로 변환하는 것 같은 중간 단계 없이요. 다른 경우라면 그런 일이 일어나는데요.

그리고 내부적으로 Pydantic은 orjson과 같은 밑바탕의 Rust 메커니즘을 사용해서 JSON으로 직렬화해요. 그래서 응답 모델로 이미 최고의 성능을 얻을 수 있어요.

기본 응답 클래스

FastAPI 클래스 인스턴스나 APIRouter를 만들 때 기본으로 사용할 응답 클래스를 지정할 수 있어요.

이걸 정의하는 파라미터는 default_response_class예요.

아래 예시에서 FastAPI는 JSON 대신 모든 path operation 에서 기본적으로 HTMLResponse를 사용해요.

Python 3.10+

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI(default_response_class=HTMLResponse)


@app.get("/items/")
async def read_items():
    return "<h1>Items</h1><p>This is a list of items.</p>"

path operations에서 response_class를 이전처럼 계속 바꿔 쓸 수 있어요.

추가 문서

responses를 사용해서 OpenAPI에 미디어 타입과 그 밖의 많은 세부 사항을 선언할 수도 있어요: Additional Responses in OpenAPI

더 알아보기 (Learn more)

이 문서는 FastAPI 공식 문서 - Custom Response를 한국어로 정리한 번역이에요. 원문에서 최신 내용과 더 다양한 예시를 확인하세요.