커스텀 응답 - 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-Type이text/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를 쓸 수도 있어요.FastAPI가
starlette.responses를fastapi.responses로 동일하게 제공하는 건 개발자(여러분)를 위한 편의일 뿐이에요. 대부분의 응답은 실제로 Starlette에서 바로 온 거예요.
Response
주요 Response 클래스예요. 다른 모든 응답들이 여기서 상속받아요.
직접 돌려줄 수 있어요.
다음 파라미터들을 받아요.
content-str또는bytes.status_code-intHTTP 상태 코드.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_code는 RedirectResponse의 기본값인 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를 한국어로 정리한 번역이에요. 원문에서 최신 내용과 더 다양한 예시를 확인하세요.