/docs 하나로 협업하기 — Swagger UI·ReDoc·OpenAPI 제대로 쓰는 법 (FastAPI 입문 3편)
FastAPI가 자동으로 만들어 주는 /docs(Swagger UI), /redoc, /openapi.json 세 주소를 실제 화면 캡처와 함께 설명한다. Try it out으로 요청을 보내고 응답을 읽는 법, tags·summary·description·examples·responses·deprecated로 문서 품질을 끌어올리는 법, openapi.json으로 프론트엔드 타입을 자동 생성하는 법, 운영 환경에서 문서를 닫는 법까지. 체험판 Swagger UI 위젯으로 할 일 API를 직접 호출해 볼 수 있다.
Server response와 Responses는 다르다. 위쪽 Server response는 방금 일어난 사실이고, 아래쪽 Responses는 코드에서 읽어 낸 약속이다. 둘이 다르면 — 예를 들어 약속엔 201만 있는데 실제로 500이 왔다면 — 그게 버그다. QA가 가장 먼저 비교하는 두 곳이다.
2-2. 체험판으로 직접 눌러 보기
서버를 띄우지 않고도 흐름을 연습할 수 있게 Swagger UI를 단순하게 옮긴 체험판을 만들었다. 5·6편의 할 일 API 여섯 개가 페이지 안의 메모리 저장소로 동작한다.
이 순서로 해 보자.
POST /tasks → Try it out → Execute. 201과 함께 id: 1인 할 일이 만들어진다.
본문의 "priority": 4를 9로 바꿔 다시 Execute. 422와 less_than_equal 에러가 온다.
GET /tasks → Try it out → Execute. 방금 만든 할 일이 목록에 보인다.
PATCH /tasks/{task_id}로 {"done": true}를 보낸 뒤, done 칸에 true를 넣고 GET /tasks를 다시 실행한다.
DELETE 후 GET /tasks/{task_id}를 부르면 404가 온다. 그런데 GET의 Responses 목록에는 404가 없다. 다음 절에서 이걸 고친다.
3. ReDoc(/redoc) — 읽기 위한 문서
/redoc은 같은 /openapi.json을 읽기 좋게 그린다. 왼쪽에 목차, 가운데에 설명과 필드 표, 오른쪽에 요청·응답 예시가 나오는 3단 구성이다. 요청을 보내는 기능은 없지만 필드 제약([1 .. 100] characters, Default: 3)이 한눈에 보여서 기획자나 외부 파트너에게 링크를 줄 때 좋다.
/openapi.json을 열면 긴 JSON이 나온다. OpenAPI라는 국제 표준 형식으로 적은 API 명세다. POST /tasks 부분만 떼어 보면 이렇다(실제 출력에서 발췌).
json
{"tags":["할 일"],"summary":"할 일 만들기","description":"새 할 일을 만든다.\n\n- **title**: 1~100자, 필수\n- **priority**: 1~5, 생략하면 3","operationId":"create_task_tasks_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskCreate"}}}},"responses":{"201":{"description":"만들어진 할 일"},"422":{"description":"Validation Error"}}}
이 JSON이 표준이라는 점이 중요하다. 사람만 읽는 게 아니라 도구가 읽는다.
도구
openapi.json으로 하는 일
openapi-typescript
프론트엔드용 TypeScript 타입을 자동 생성 (아래 예시)
Postman · Insomnia · Bruno
“Import”로 불러오면 모든 API가 요청 모음으로 만들어진다
OpenAPI Generator
Kotlin·Swift·Java 등 모바일·다른 언어용 클라이언트 코드 생성
Schemathesis
명세를 읽어 이상한 값을 자동으로 쏘아 보는 테스트
예를 들어 프론트엔드 개발자는 이 명령 한 줄로 백엔드의 모델을 TypeScript 타입으로 받는다.
TaskCreate: {
/**
* Title
* @description 할 일 제목
* @example 보고서 초안 쓰기
*/title: string;
// ...
};
백엔드에서 필드 이름을 title에서 name으로 바꾸면, 프론트엔드는 타입을 다시 생성하는 순간 컴파일 에러로 알게 된다. 슬랙 메시지로 “필드 이름 바뀌었어요”를 알릴 필요가 없다.
5. 문서 품질을 끌어올리는 여덟 가지
자동으로 생기는 문서도 쓸 만하지만, 몇 줄만 더 적으면 “물어볼 필요 없는” 문서가 된다. 이 편의 예제 코드 전체다.
python
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
tags_metadata = [
{"name": "할 일", "description": "할 일을 만들고, 보고, 고치고, 지운다."},
{"name": "운영", "description": "서버 상태 확인용. 프론트엔드는 쓰지 않는다."},
]
app = FastAPI(
title="할 일 API",
version="1.0.0",
description="""
팀 공용 **할 일 관리 API**입니다.
* 모든 시간은 한국 시간(KST)입니다.
* 문의: 백엔드 채널 `#team-backend`
""",
openapi_tags=tags_metadata,
)
classTaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100, description="할 일 제목", examples=["보고서 초안 쓰기"])
priority: int = Field(default=3, ge=1, le=5, description="1(낮음) ~ 5(높음)", examples=[4])
classTask(BaseModel):
id: int
title: str
priority: int
done: boolclassMessage(BaseModel):
detail: str
tasks: dict[int, Task] = {}
@app.post("/tasks",
response_model=Task,
status_code=status.HTTP_201_CREATED,
tags=["할 일"],
summary="할 일 만들기",
response_description="만들어진 할 일",
)defcreate_task(payload: TaskCreate):
"""
새 할 일을 만든다.
- **title**: 1~100자, 필수
- **priority**: 1~5, 생략하면 3
"""
task = Task(id=len(tasks) + 1, done=False, **payload.model_dump())
tasks[task.id] = task
return task
@app.get("/tasks", response_model=list[Task], tags=["할 일"], summary="할 일 목록")deflist_tasks(done: bool | None = None):
return [t for t in tasks.values() if done isNoneor t.done == done]
@app.get("/tasks/{task_id}",
response_model=Task,
tags=["할 일"],
summary="할 일 하나 보기",
responses={404: {"model": Message, "description": "그 번호의 할 일이 없음"}},
)defread_task(task_id: int):
if task_id notin tasks:
raise HTTPException(status_code=404, detail=f"{task_id}번 할 일이 없습니다")
return tasks[task_id]
@app.get("/todos", tags=["할 일"], summary="(구버전) 할 일 목록", deprecated=True)deflist_todos_old():
returnlist(tasks.values())
@app.get("/health", tags=["운영"], summary="서버 상태 확인")defhealth():
return {"status": "ok"}
@app.get("/internal/debug", include_in_schema=False)defdebug():
return {"tasks": len(tasks)}
코드의 각 부분이 화면 어디에 나타나는지 정리하면 이렇다.
코드
화면에 나타나는 곳
안 쓰면
① FastAPI(title, version, description)
맨 위 제목·버전 뱃지·설명 문단
“FastAPI 0.1.0”, 설명 없음
② tags=[...] + openapi_tags
라우트 묶음과 묶음 설명
전부 “default” 한 묶음 — 라우트가 30개면 찾기 힘들다
③ summary="할 일 만들기"
라우트 줄 오른쪽 요약
함수 이름에서 자동 생성(“Create Task”)
④ 함수 docstring """..."""
펼쳤을 때의 설명(마크다운)
설명 없음
⑤ Field(description, examples)
Schemas의 필드 설명, Try it out의 기본 예시 값
예시가 "string", 0 같은 무의미한 값
⑥ responses={404: {...}}
Responses 목록에 404와 그 모양 추가
404가 실제로 나와도 문서에는 없음
⑦ deprecated=True
취소선 + 회색 줄
구버전 API를 계속 쓰는 사람이 생김
⑧ include_in_schema=False
문서에서 숨김(동작은 함)
내부 디버그용 API가 모두에게 보임
⚠️
⑥이 가장 자주 빠진다. FastAPI는 코드에서 raise HTTPException(404)를 찾아 문서에 넣어 주지 않는다. 자동으로 적히는 것은 성공 응답과 422뿐이다. “없을 때 404를 준다”는 약속은 responses=로 직접 적어야 프론트엔드가 안다. 2-2절 체험판의 GET에 404가 없던 이유가 이것이다.
🔒
⑧은 보안 장치가 아니다.include_in_schema=False는 문서에서만 숨긴다. 주소를 아는 사람은 여전히 호출할 수 있다. 정말 막아야 하는 API는 인증을 걸어야 한다(7편에서 의존성 주입으로 다룬다).
6. 운영 환경에서 문서 닫기
/docs는 개발·스테이징에서는 보물이지만, 외부에 공개된 운영 서버에서는 공격자에게도 친절한 지도가 된다. 공개 API가 아니라면 운영에서는 닫는 경우가 많다.
python
import os
from fastapi import FastAPI
IS_PROD = os.getenv("APP_ENV") == "production"
app = FastAPI(
title="할 일 API",
docs_url=Noneif IS_PROD else"/docs",
redoc_url=Noneif IS_PROD else"/redoc",
openapi_url=Noneif IS_PROD else"/openapi.json",
)
직접 확인해 보면, docs_url=None, redoc_url=None만 주면 두 화면은 404가 되지만 /openapi.json은 여전히 200으로 열린다. 명세까지 닫으려면 openapi_url=None을 줘야 하고, 그러면 세 주소가 모두 404가 된다.
7. /docs 중심 협업 흐름
/docs가 있는 팀은 일하는 순서가 바뀐다. 백엔드 구현이 끝날 때까지 프론트엔드가 기다릴 필요가 없다.
① 모양 먼저
백엔드가 라우트와 Pydantic 모델만 먼저 만들고, 함수 안에서는 가짜 데이터를 돌려준다. 여기까지 30분이면 된다.
② 리뷰
프론트엔드·기획자가 /docs를 보고 “목록에 작성자 이름도 필요해요”, “완료 필터가 있어야 해요”를 코드 짜기 전에 말한다. 고치는 비용이 가장 싼 시점이다.
③ 동시 개발
프론트엔드는 확정된 모양으로 화면을 만들고(openapi-typescript로 타입 생성), 백엔드는 가짜 데이터를 진짜 DB 로직으로 바꾼다.
④ 검증
QA는 /docs의 Try it out으로 경계값(빈 제목, 우선순위 0·6)을 넣어 보고, 문제가 있으면 Curl을 복사해 이슈에 붙인다.
8. 팀 문서 체크리스트
PR을 올리기 전에 /docs를 열어 이것만 확인하자.
/docs 체크리스트
☐ 새 라우트에 tags가 달려 있어 올바른 묶음에 들어갔나
☐ summary가 한국어로 “무엇을 하는지” 말하나 (“Create Task” 그대로 두지 않기)
☐ 요청 모델의 필드에 description과 현실적인 examples가 있나
☐ 404·409 등 실제로 나올 수 있는 에러를 responses=로 적었나
☐ Try it out → Execute로 한 번 이상 직접 실행해 봤나
☐ 없어질 API에는 deprecated=True를 달았나
정리
📄
세 주소
/docs는 눌러 보는 문서, /redoc은 읽는 문서, /openapi.json은 도구가 읽는 원본. 셋 다 코드에서 자동으로 생기고 코드와 절대 어긋나지 않는다.
✍️
몇 줄로 좋은 문서
tags · summary · docstring · Field(description, examples) · responses · deprecated. 특히 404는 자동으로 적히지 않으니 직접 적는다.
🤝
계약서로 일하기
모양을 먼저 만들어 /docs로 리뷰하고, 프론트엔드는 openapi.json으로 타입을 생성하고, QA는 Curl로 버그를 제보한다. 운영에서는 문서를 닫는다.
이제 요청을 보내고 확인하는 도구가 생겼다. 그런데 이 편의 예제 코드에는 class TaskCreate(BaseModel), Field(min_length=1) 같은 낯선 줄이 계속 나왔다. 문서의 Schemas 칸을 채운 것도 이 줄들이다. 다음 편에서는 CRUD로 넘어가기 전에, FastAPI의 절반이라고 해도 될 Pydantic을 FastAPI 없이 따로 떼어 기초부터 익힌다.
실행 환경과 캡처. Swagger UI·ReDoc 화면은 이 글의 예제 코드를 FastAPI 0.141.1로 실제 실행해 캡처했다. openapi.json 발췌와 TypeScript 타입은 각각 서버의 실제 출력과 openapi-typescript 7.13.0의 실제 생성 결과다. 운영 환경 문서 닫기(docs_url·redoc_url·openapi_url)의 404/200 결과도 직접 확인했다. 체험판 위젯은 실제 Swagger UI의 흐름을 단순화한 것으로, 서버 없이 브라우저 메모리에서 동작한다. 삽화는 코어닷투데이가 생성했다.