coredot.today
/docs 하나로 협업하기 — Swagger UI·ReDoc·OpenAPI 제대로 쓰는 법 (FastAPI 입문 3편)
블로그로 돌아가기
FastAPISwagger UIReDocOpenAPIAPI 문서협업프론트엔드openapi-typescripttagssummary입문튜토리얼

/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를 직접 호출해 볼 수 있다.

코어닷투데이2026-09-2732분

들어가며 — 문서를 쓰지 않았는데 문서가 있다

/docs 하나로 협업크게 보기

2편에서 서버를 띄울 때 터미널에 이런 줄이 있었다.

fastapi dev main.py

🌐 Server started at http://127.0.0.1:8000

   Documentation at http://127.0.0.1:8000/docs

우리는 문서를 한 줄도 쓰지 않았다. 그런데 FastAPI는 “문서는 여기 있다”고 말한다. 1편에서 FastAPI의 가장 큰 장점을 “타입 힌트 한 줄이 검증·변환·문서를 동시에 만든다” 고 했다. 이번 편은 그중 문서 이야기다.

협업 관점에서 이 편은 시리즈에서 가장 중요하다. /docs를 제대로 쓰는 팀은 “API 주소가 뭐였죠?”, “이 필드 필수예요?”, “에러는 어떻게 와요?”라는 질문이 거의 사라진다.

이 편의 예제는 5·6편에서 만들 할 일 API를 미리 가져와 쓴다. 코드에 나오는 Pydantic 모델은 4편에서, 상태 코드와 CRUD는 5·6편에서 자세히 설명하니, 지금은 “이렇게 쓰면 문서에 이렇게 나온다”에만 집중하자.

1. 공짜로 생기는 세 개의 주소

주소무엇인가누가 주로 보나
/docsSwagger UI — 문서를 보면서 그 자리에서 요청을 보내 볼 수 있는 화면백엔드·프론트엔드 개발자, QA — “눌러 보는” 문서
/redocReDoc — 같은 내용을 읽기 좋게 정리한 3단 레이아웃 문서기획자, 외부 파트너, 신규 입사자 — “읽는” 문서
/openapi.jsonOpenAPI 명세 — 위 두 화면의 원본 데이터(JSON)프로그램 — 코드 생성기, Postman, 테스트 도구

셋의 관계는 이렇다.

파이썬 코드
라우트 + 타입 힌트 + 모델
→
/openapi.json
FastAPI가 자동 생성
→
/docs · /redoc
JSON을 화면으로 그림

코드가 바뀌면 문서가 따라 바뀐다. 누가 문서를 고칠 필요가 없고, 그래서 문서와 코드가 어긋나지 않는다. 위키에 따로 적은 API 문서가 석 달 뒤면 거짓말이 되는 것과 가장 크게 다른 점이다.

2. Swagger UI(/docs) 읽는 법

아래는 이 편의 예제 코드를 실제로 띄우고 /docs를 연 화면이다.

FastAPI가 만든 Swagger UI 첫 화면크게 보기

위에서부터 읽으면 된다.

제목·버전
“할 일 API 1.0.0”과 설명 문단. FastAPI(title=..., version=..., description=...)에서 나온다. 설명에는 마크다운(굵게, 목록, 코드)을 쓸 수 있다.
태그 묶음
“할 일”, “운영” 같은 그룹. 라우트에 tags=["할 일"]을 달면 여기 모인다. 태그가 없으면 전부 “default” 한 묶음이 된다.
라우트 한 줄
색깔 = 메서드(초록 POST, 파랑 GET, 주황 PUT, 청록 PATCH, 빨강 DELETE) + 경로 + 요약. 취소선이 그어진 회색 줄은 deprecated(곧 없어질 API)다.
Schemas
요청·응답에 쓰이는 데이터 모양(Pydantic 모델)의 목록. 필드 이름, 타입, 필수 여부, 제약 조건이 다 나온다.

2-1. Try it out — 브라우저에서 요청 보내기

라우트 줄을 누르면 펼쳐지고, 오른쪽의 Try it out 버튼을 누르면 입력칸이 열린다. 값을 넣고 Execute를 누르면 실제 서버로 요청이 간다. 아래는 POST /tasks를 실행한 실제 화면이다.

POST /tasks를 Try it out으로 실행한 화면크게 보기

펼친 화면에서 봐야 할 곳은 다섯 군데다.

영역무엇을 보여 주나협업에서 쓰는 법
설명 (맨 위)함수의 docstring("""...""")이 그대로 나온다필드 규칙, 주의사항을 여기에 적어 둔다
Request body보낼 JSON. 처음엔 examples로 지정한 예시 값이 채워져 있다프론트엔드는 이 예시를 복사해서 시작한다
Curl방금 보낸 요청을 터미널 명령으로 바꾼 것버그 제보할 때 이걸 그대로 붙여 넣는다 — 재현이 한 번에 된다
Server response실제로 돌아온 상태 코드·본문·헤더“지금 이 서버가 실제로 이렇게 답한다”
Responses (맨 아래)문서에 약속된 응답 목록(201, 422)과 각 응답의 모양“이 API는 이렇게 답하기로 약속했다”
💡
Server response와 Responses는 다르다. 위쪽 Server response는 방금 일어난 사실이고, 아래쪽 Responses는 코드에서 읽어 낸 약속이다. 둘이 다르면 — 예를 들어 약속엔 201만 있는데 실제로 500이 왔다면 — 그게 버그다. QA가 가장 먼저 비교하는 두 곳이다.

2-2. 체험판으로 직접 눌러 보기

서버를 띄우지 않고도 흐름을 연습할 수 있게 Swagger UI를 단순하게 옮긴 체험판을 만들었다. 5·6편의 할 일 API 여섯 개가 페이지 안의 메모리 저장소로 동작한다.

이 순서로 해 보자.

  1. POST /tasks → Try it out → Execute. 201과 함께 id: 1인 할 일이 만들어진다.
  2. 본문의 "priority": 4를 9로 바꿔 다시 Execute. 422와 less_than_equal 에러가 온다.
  3. GET /tasks → Try it out → Execute. 방금 만든 할 일이 목록에 보인다.
  4. PATCH /tasks/{task_id}로 {"done": true}를 보낸 뒤, done 칸에 true를 넣고 GET /tasks를 다시 실행한다.
  5. DELETE 후 GET /tasks/{task_id}를 부르면 404가 온다. 그런데 GET의 Responses 목록에는 404가 없다. 다음 절에서 이걸 고친다.

3. ReDoc(/redoc) — 읽기 위한 문서

/redoc은 같은 /openapi.json을 읽기 좋게 그린다. 왼쪽에 목차, 가운데에 설명과 필드 표, 오른쪽에 요청·응답 예시가 나오는 3단 구성이다. 요청을 보내는 기능은 없지만 필드 제약([1 .. 100] characters, Default: 3)이 한눈에 보여서 기획자나 외부 파트너에게 링크를 줄 때 좋다.

같은 API를 ReDoc으로 본 화면크게 보기

4. OpenAPI — 프론트엔드와 백엔드 사이의 계약서

OpenAPI는 프론트엔드와 백엔드의 계약서크게 보기

/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 GeneratorKotlin·Swift·Java 등 모바일·다른 언어용 클라이언트 코드 생성
Schemathesis명세를 읽어 이상한 값을 자동으로 쏘아 보는 테스트

예를 들어 프론트엔드 개발자는 이 명령 한 줄로 백엔드의 모델을 TypeScript 타입으로 받는다.

bash
npx openapi-typescript http://127.0.0.1:8000/openapi.json -o src/api.d.ts

생성된 파일(실제 출력에서 발췌)에는 백엔드에서 적은 설명과 예시까지 주석으로 들어온다.

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,
)


class TaskCreate(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])


class Task(BaseModel):
    id: int
    title: str
    priority: int
    done: bool


class Message(BaseModel):
    detail: str


tasks: dict[int, Task] = {}


@app.post(
    "/tasks",
    response_model=Task,
    status_code=status.HTTP_201_CREATED,
    tags=["할 일"],
    summary="할 일 만들기",
    response_description="만들어진 할 일",
)
def create_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="할 일 목록")
def list_tasks(done: bool | None = None):
    return [t for t in tasks.values() if done is None or t.done == done]


@app.get(
    "/tasks/{task_id}",
    response_model=Task,
    tags=["할 일"],
    summary="할 일 하나 보기",
    responses={404: {"model": Message, "description": "그 번호의 할 일이 없음"}},
)
def read_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail=f"{task_id}번 할 일이 없습니다")
    return tasks[task_id]


@app.get("/todos", tags=["할 일"], summary="(구버전) 할 일 목록", deprecated=True)
def list_todos_old():
    return list(tasks.values())


@app.get("/health", tags=["운영"], summary="서버 상태 확인")
def health():
    return {"status": "ok"}


@app.get("/internal/debug", include_in_schema=False)
def debug():
    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=None if IS_PROD else "/docs",
    redoc_url=None if IS_PROD else "/redoc",
    openapi_url=None if 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의 흐름을 단순화한 것으로, 서버 없이 브라우저 메모리에서 동작한다. 삽화는 코어닷투데이가 생성했다.