Django-Ninja로 API 문서 자동화하기: 프론트엔드 협업을 위한 완벽 가이드
목차
- API 문서화가 중요한 이유
- Django-Ninja 소개
- 기본 설정과 첫 API 문서
- 스키마 설계 전략
- 프론트엔드 협업을 위한 문서화 모범 사례
- 고급 기능과 커스터마이징
- 실전 적용 팁
- 결론
API 문서화가 중요한 이유
백엔드 개발자로서 가장 자주 겪는 문제 중 하나는 프론트엔드 개발자와의 커뮤니케이션 비용입니다. “이 API 파라미터가 뭔가요?”, “에러 코드 400일 때 어떤 메시지가 오나요?”, “이 필드는 필수인가요?” 같은 질문들이 슬랙 메시지로 쏟아지고, 결국 회의를 하거나 구두로 설명하느라 시간을 낭비하게 됩니다.
더 큰 문제는 구두 소통은 기록이 남지 않는다는 점입니다. 3개월 후 새로운 프론트엔드 개발자가 합류하면 같은 질문을 다시 받게 되고, 또 설명해야 합니다. API 명세가 변경되면 노션이나 컨플루언스 문서를 수동으로 업데이트해야 하는데, 바쁜 스프린트 기간에는 이를 잊어버리기 쉽습니다. 결과적으로 문서와 실제 코드가 불일치하게 되고, 문서를 더 이상 신뢰할 수 없게 됩니다.
해결책은 명확합니다: 코드 자체가 문서가 되어야 합니다. API 엔드포인트를 정의할 때 사용하는 코드가 자동으로 문서를 생성한다면, 문서는 항상 최신 상태를 유지하고, 별도의 유지보수 비용도 들지 않습니다. Django-Ninja는 바로 이런 철학을 기반으로 설계된 프레임워크입니다.
Django-Ninja 소개
Django-Ninja는 FastAPI에서 영감을 받아 만들어진 Django용 API 프레임워크입니다. Django REST Framework(DRF)가 이미 있는데 왜 Django-Ninja를 사용해야 할까요? 핵심은 타입 힌트 기반 자동 검증과 문서화입니다.
# DRF 방식 - Serializer를 별도로 정의
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
# Django-Ninja 방식 - 타입 힌트만으로 완성
from ninja import NinjaAPI, Schema
api = NinjaAPI()
class UserSchema(Schema):
id: int
username: str
email: str
@api.get("/users/{user_id}", response=UserSchema)
def get_user(request, user_id: int):
return User.objects.get(id=user_id)
Django-Ninja의 가장 큰 장점은 자동으로 생성되는 OpenAPI(Swagger) 문서입니다. 위 코드를 작성하면 /api/docs에서 인터랙티브한 API 문서를 즉시 확인할 수 있습니다. 프론트엔드 개발자는 이 페이지에서 직접 API를 테스트하고, 파라미터 형식을 확인하며, 응답 구조를 파악할 수 있습니다.
더 중요한 것은 타입 안정성입니다. Python 3.6+의 타입 힌트를 활용하면 IDE에서 자동완성을 받을 수 있고, mypy 같은 도구로 타입 체크를 할 수 있습니다. 이는 런타임 에러를 줄이고 코드 품질을 높이는 데 큰 도움이 됩니다.
기본 설정과 첫 API 문서
Django-Ninja를 시작하는 것은 매우 간단합니다. 먼저 패키지를 설치합니다:
pip install django-ninja
기본적인 프로젝트 구조를 다음과 같이 설정할 수 있습니다:
# myproject/api.py
from ninja import NinjaAPI
api = NinjaAPI(
title="My Project API",
version="1.0.0",
description="프론트엔드 협업을 위한 API 문서",
)
# myproject/urls.py
from django.contrib import admin
from django.urls import path
from .api import api
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', api.urls), # /api/docs 에서 문서 확인 가능
]
이제 실제 API 엔드포인트를 만들어 봅시다. 블로그 시스템을 예로 들어보겠습니다:
# blog/schemas.py
from ninja import Schema
from datetime import datetime
from typing import Optional, List
class PostSchema(Schema):
id: int
title: str
content: str
author: str
created_at: datetime
updated_at: datetime
tags: List[str]
is_published: bool
class PostCreateSchema(Schema):
title: str
content: str
tags: Optional[List[str]] = []
class PostUpdateSchema(Schema):
title: Optional[str] = None
content: Optional[str] = None
tags: Optional[List[str]] = None
is_published: Optional[bool] = None
# blog/api.py
from ninja import Router
from .schemas import PostSchema, PostCreateSchema, PostUpdateSchema
from .models import Post
from typing import List
router = Router()
@router.get("/posts", response=List[PostSchema])
def list_posts(request, is_published: bool = True):
"""
게시글 목록을 조회합니다.
- **is_published**: 발행된 게시글만 조회 (기본값: True)
"""
posts = Post.objects.filter(is_published=is_published)
return posts
@router.get("/posts/{post_id}", response=PostSchema)
def get_post(request, post_id: int):
"""특정 게시글의 상세 정보를 조회합니다."""
return Post.objects.get(id=post_id)
@router.post("/posts", response=PostSchema)
def create_post(request, payload: PostCreateSchema):
"""
새로운 게시글을 생성합니다.
요청 본문에 title과 content는 필수입니다.
"""
post = Post.objects.create(
title=payload.title,
content=payload.content,
author=request.user.username,
)
post.tags.set(payload.tags)
return post
# myproject/api.py에 라우터 등록
from blog.api import router as blog_router
api.add_router("/blog", blog_router, tags=["Blog"])
이 코드만으로 /api/docs에 접속하면 완전한 API 문서가 생성됩니다. 각 엔드포인트의 파라미터, 요청/응답 형식, 그리고 docstring에 작성한 설명까지 모두 포함됩니다. 프론트엔드 개발자는 “Try it out” 버튼을 눌러 바로 API를 테스트할 수 있습니다.
스키마 설계 전략
프론트엔드 개발자와의 원활한 협업을 위해서는 스키마를 전략적으로 설계해야 합니다. 가장 중요한 원칙은 명확성과 일관성입니다.
1. 명확한 필드 설명 추가하기
from ninja import Schema, Field
from typing import Optional
class UserProfileSchema(Schema):
id: int = Field(..., description="사용자 고유 ID")
username: str = Field(..., description="사용자명 (3-20자, 영문/숫자)",
min_length=3, max_length=20)
email: str = Field(..., description="이메일 주소")
avatar_url: Optional[str] = Field(None, description="프로필 이미지 URL (없으면 null)")
bio: Optional[str] = Field(None, description="자기소개 (최대 500자)",
max_length=500)
created_at: str = Field(..., description="가입일시 (ISO 8601 형식)")
class Config:
schema_extra = {
"example": {
"id": 123,
"username": "johndoe",
"email": "john@example.com",
"avatar_url": "https://cdn.example.com/avatars/123.jpg",
"bio": "Python 백엔드 개발자입니다.",
"created_at": "2026-01-18T10:30:00Z"
}
}
Field의 description 파라미터는 Swagger 문서에 그대로 표시됩니다. 프론트엔드 개발자가 각 필드의 의미를 정확히 이해할 수 있도록 구체적으로 작성하세요. Config.schema_extra에 예시를 추가하면 문서에서 실제 데이터 형태를 바로 확인할 수 있습니다.
2. 에러 응답 명시하기
프론트엔드에서 에러 핸들링을 하려면 에러 응답 구조를 알아야 합니다:
from ninja import Schema
from typing import Dict, Any
class ErrorSchema(Schema):
detail: str = Field(..., description="에러 상세 메시지")
code: str = Field(..., description="에러 코드 (예: VALIDATION_ERROR)")
class ValidationErrorSchema(Schema):
detail: Dict[str, Any] = Field(...,
description="필드별 검증 에러 메시지")
class Config:
schema_extra = {
"example": {
"detail": {
"username": ["이미 사용 중인 사용자명입니다."],
"email": ["올바른 이메일 형식이 아닙니다."]
}
}
}
@router.post("/users", response={201: UserProfileSchema, 400: ValidationErrorSchema})
def create_user(request, payload: UserCreateSchema):
"""
새로운 사용자를 생성합니다.
성공 시 201 상태 코드와 함께 생성된 사용자 정보를 반환합니다.
검증 실패 시 400 상태 코드와 함께 에러 상세를 반환합니다.
"""
# 구현 로직...
pass
response 파라미터에 딕셔너리를 전달하면 상태 코드별로 다른 응답 스키마를 정의할 수 있습니다. 이렇게 하면 프론트엔드 개발자가 각 상황에서 어떤 데이터 구조를 받을지 명확히 알 수 있습니다.
3. 페이지네이션 일관성 유지하기
from typing import Generic, TypeVar, List
T = TypeVar('T')
class PaginatedResponse(Schema, Generic[T]):
count: int = Field(..., description="전체 항목 수")
next: Optional[str] = Field(None, description="다음 페이지 URL")
previous: Optional[str] = Field(None, description="이전 페이지 URL")
results: List[T] = Field(..., description="현재 페이지 항목 목록")
@router.get("/posts", response=PaginatedResponse[PostSchema])
def list_posts(request, page: int = 1, page_size: int = 20):
"""
게시글 목록을 페이지네이션하여 반환합니다.
- **page**: 페이지 번호 (기본값: 1)
- **page_size**: 페이지당 항목 수 (기본값: 20, 최대: 100)
"""
# 페이지네이션 로직...
return {
"count": total_count,
"next": next_url,
"previous": prev_url,
"results": posts
}
모든 목록 API에서 동일한 페이지네이션 구조를 사용하면 프론트엔드에서 재사용 가능한 컴포넌트를 만들 수 있습니다. Generic 타입을 활용하면 타입 안정성도 유지할 수 있습니다.
프론트엔드 협업을 위한 문서화 모범 사례
자동 생성되는 문서도 좋지만, 프론트엔드 개발자와의 협업을 극대화하려면 몇 가지 추가 전략이 필요합니다.
1. TypeScript 타입 자동 생성
OpenAPI 스펙에서 TypeScript 타입을 자동 생성하면 프론트엔드 개발자가 타입 안정성을 확보할 수 있습니다:
# openapi-typescript 설치
npm install -D openapi-typescript
# API 스키마를 TypeScript 타입으로 변환
npx openapi-typescript http://localhost:8000/api/openapi.json -o src/types/api.ts
이렇게 생성된 타입을 프론트엔드에서 사용할 수 있습니다:
// src/types/api.ts (자동 생성됨)
export interface PostSchema {
id: number;
title: string;
content: string;
author: string;
created_at: string;
tags: string[];
is_published: boolean;
}
// src/services/api.ts
import type { PostSchema } from '../types/api';
async function getPosts(): Promise<PostSchema[]> {
const response = await fetch('/api/blog/posts');
return response.json();
}
이를 CI/CD 파이프라인에 통합하면 백엔드 API가 변경될 때마다 프론트엔드 타입도 자동으로 업데이트됩니다.
2. 인증 문서화하기
대부분의 실제 API는 인증이 필요합니다. 이를 명확히 문서화해야 합니다:
from ninja.security import HttpBearer
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
# 토큰 검증 로직
if valid_token(token):
return token
return None
api = NinjaAPI(auth=AuthBearer())
# 특정 엔드포인트만 인증 필요
@router.post("/posts", auth=AuthBearer())
def create_post(request, payload: PostCreateSchema):
pass
# 인증 불필요한 엔드포인트
@router.get("/posts", auth=None)
def list_posts(request):
pass
문서에 인증 방법을 추가하려면:
api = NinjaAPI(
title="My API",
description="""
## 인증 방법
대부분의 엔드포인트는 JWT 토큰 인증이 필요합니다.
1. `/api/auth/login` 엔드포인트로 로그인
2. 응답에서 받은 `access_token`을 사용
3. 요청 헤더에 `Authorization: Bearer <token>` 추가
### 예시
```
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..." \\
https://api.example.com/api/blog/posts
```
""",
version="1.0.0"
)
3. 환경별 문서 URL 제공하기
개발, 스테이징, 프로덕션 환경마다 다른 베이스 URL을 제공하면 프론트엔드 개발자가 쉽게 전환할 수 있습니다:
# settings.py
NINJA_DOCS_SERVERS = [
{"url": "http://localhost:8000", "description": "로컬 개발 환경"},
{"url": "https://api-staging.example.com", "description": "스테이징 환경"},
{"url": "https://api.example.com", "description": "프로덕션 환경"},
]
# api.py
from django.conf import settings
api = NinjaAPI(
title="My API",
version="1.0.0",
servers=settings.NINJA_DOCS_SERVERS,
)
Swagger UI에서 환경을 드롭다운으로 선택할 수 있게 되어, 프론트엔드 개발자가 각 환경에서 API를 테스트하기 쉬워집니다.
4. 변경 이력 문서화하기
API 버전 관리와 변경 이력을 문서화하면 프론트엔드 팀이 업데이트에 대응하기 쉽습니다:
api = NinjaAPI(
title="My Project API",
version="2.1.0",
description="""
## 변경 이력
### v2.1.0 (2026-01-18)
- `POST /api/blog/posts`: `tags` 필드 추가
- `GET /api/blog/posts`: 쿼리 파라미터 `sort` 추가 (created_at, title)
### v2.0.0 (2026-01-01) - Breaking Changes
- `GET /api/blog/posts`: 응답 구조 변경 (페이지네이션 적용)
- `POST /api/auth/login`: `username` 대신 `email` 사용
### v1.0.0 (2025-12-01)
- 초기 릴리스
"""
)
중요한 변경사항은 슬랙이나 이메일로도 공지하되, 문서에 기록을 남겨두면 나중에 참고하기 좋습니다.
고급 기능과 커스터마이징
Django-Ninja는 다양한 고급 기능을 제공합니다. 프론트엔드 협업을 더욱 효율적으로 만들 수 있는 몇 가지를 소개합니다.
1. 파일 업로드 문서화
파일 업로드 API는 프론트엔드 개발자가 자주 헷갈려하는 부분입니다:
from ninja import File, UploadedFile, Schema
class ImageUploadResponse(Schema):
url: str = Field(..., description="업로드된 이미지 URL")
width: int = Field(..., description="이미지 너비 (픽셀)")
height: int = Field(..., description="이미지 높이 (픽셀)")
size: int = Field(..., description="파일 크기 (바이트)")
@router.post("/upload/image", response=ImageUploadResponse)
def upload_image(request, file: UploadedFile = File(...)):
"""
이미지 파일을 업로드합니다.
- **지원 형식**: JPEG, PNG, WebP
- **최대 크기**: 5MB
- **권장 해상도**: 1920x1080 이하
### 사용 예시 (JavaScript)
```javascript
const formData = new FormData();
formData.append('file', imageFile);
const response = await fetch('/api/upload/image', {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${token}`
}
});
```
"""
# 파일 처리 로직...
return {
"url": uploaded_url,
"width": image.width,
"height": image.height,
"size": file.size
}
# 여러 파일 업로드
@router.post("/upload/images")
def upload_multiple_images(request, files: List[UploadedFile] = File(...)):
"""
여러 이미지를 한 번에 업로드합니다 (최대 10개).
"""
pass
2. 필터링 파라미터 문서화
복잡한 필터링 옵션은 스키마로 정리하면 문서가 깔끔해집니다:
from ninja import Schema
from typing import Optional
from datetime import date
class PostFilterSchema(Schema):
search: Optional[str] = Field(None, description="제목 또는 내용에서 검색")
author: Optional[str] = Field(None, description="작성자 사용자명")
tags: Optional[List[str]] = Field(None, description="태그 목록 (OR 조건)")
is_published: Optional[bool] = Field(None, description="발행 여부")
created_after: Optional[date] = Field(None, description="이 날짜 이후 작성")
created_before: Optional[date] = Field(None, description="이 날짜 이전 작성")
class Config:
schema_extra = {
"example": {
"search": "Django",
"tags": ["python", "backend"],
"is_published": True,
"created_after": "2026-01-01"
}
}
@router.get("/posts", response=List[PostSchema])
def list_posts(request, filters: PostFilterSchema = Query(...)):
"""
다양한 조건으로 게시글을 필터링합니다.
모든 필터는 AND 조건으로 적용되며, tags만 OR 조건입니다.
"""
queryset = Post.objects.all()
if filters.search:
queryset = queryset.filter(
Q(title__icontains=filters.search) |
Q(content__icontains=filters.search)
)
if filters.author:
queryset = queryset.filter(author=filters.author)
# 나머지 필터 적용...
return queryset
3. 웹훅 문서화하기
시스템이 웹훅을 보내는 경우, 이를 명확히 문서화해야 합니다:
class WebhookPostCreatedSchema(Schema):
"""
게시글 생성 시 전송되는 웹훅 페이로드입니다.
이 웹훅은 새 게시글이 발행될 때 등록된 URL로 POST 요청을 보냅니다.
### 헤더
- `X-Webhook-Signature`: HMAC-SHA256 서명
- `X-Webhook-Event`: "post.created"
### 재시도 정책
- 실패 시 1분, 5분, 30분 간격으로 최대 3회 재시도
- 2xx 응답을 성공으로 간주
"""
event: str = Field("post.created", description="이벤트 타입")
timestamp: str = Field(..., description="이벤트 발생 시각 (ISO 8601)")
data: PostSchema = Field(..., description="생성된 게시글 정보")
class Config:
schema_extra = {
"example": {
"event": "post.created",
"timestamp": "2026-01-18T10:30:00Z",
"data": {
"id": 123,
"title": "Django-Ninja 튜토리얼",
"content": "...",
"author": "johndoe",
"created_at": "2026-01-18T10:30:00Z",
"tags": ["django", "python"],
"is_published": True
}
}
}
이 스키마는 실제 API 엔드포인트가 아니라 웹훅 문서화 목적으로만 사용됩니다. 프론트엔드 개발자가 웹훅을 수신하는 서버를 구축할 때 참고할 수 있습니다.
4. API 버저닝
API 버전을 관리하려면 라우터를 분리하는 것이 좋습니다:
# api/v1/posts.py
from ninja import Router
router_v1 = Router()
@router_v1.get("/posts")
def list_posts_v1(request):
"""V1: 간단한 목록 반환"""
return Post.objects.all()[:20]
# api/v2/posts.py
router_v2 = Router()
@router_v2.get("/posts", response=PaginatedResponse[PostSchema])
def list_posts_v2(request, page: int = 1):
"""V2: 페이지네이션 적용"""
# 구현...
pass
# api.py
from api.v1.posts import router_v1
from api.v2.posts import router_v2
api_v1 = NinjaAPI(version="1.0.0", urls_namespace="api-v1")
api_v1.add_router("/blog", router_v1)
api_v2 = NinjaAPI(version="2.0.0", urls_namespace="api-v2")
api_v2.add_router("/blog", router_v2)
# urls.py
urlpatterns = [
path('api/v1/', api_v1.urls),
path('api/v2/', api_v2.urls),
]
이렇게 하면 /api/v1/docs와 /api/v2/docs에서 각각의 문서를 확인할 수 있습니다. 프론트엔드 팀은 점진적으로 새 버전으로 마이그레이션할 수 있습니다.
실전 적용 팁
실제 프로젝트에서 Django-Ninja를 도입할 때 유용한 팁들을 공유합니다.
1. 개발 환경에서만 문서 제공하기
프로덕션에서는 보안상 API 문서를 비활성화하는 것이 좋습니다:
from django.conf import settings
api = NinjaAPI(
title="My API",
version="1.0.0",
docs_url="/docs" if settings.DEBUG else None, # 프로덕션에서는 None
)
또는 인증된 사용자만 접근하도록 제한할 수 있습니다:
from ninja import NinjaAPI
from ninja.security import django_auth
api = NinjaAPI(
title="My API",
version="1.0.0",
docs_decorator=lambda func: django_auth(func), # 로그인 필요
)
2. CI/CD에 문서 검증 추가하기
OpenAPI 스펙을 검증하는 단계를 CI에 추가하면 문서 품질을 유지할 수 있습니다:
# .github/workflows/api-docs.yml
name: API Documentation
on: [pull_request]
jobs:
validate-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install django-ninja openapi-spec-validator
- name: Generate OpenAPI schema
run: |
python manage.py generate_openapi_schema > openapi.json
- name: Validate OpenAPI schema
run: |
openapi-spec-validator openapi.json
- name: Check for breaking changes
run: |
# 이전 버전과 비교해 breaking changes 검사
python scripts/check_api_changes.py
3. 프론트엔드 팀과의 협업 워크플로우
실제로 효과적이었던 협업 방법입니다:
- API 설계 먼저: 기능 개발 전에 스키마와 엔드포인트를 먼저 정의하고 PR을 올립니다.
- 문서 리뷰: 프론트엔드 개발자가
/api/docs를 확인하고 피드백을 남깁니다. - Mock 데이터: 백엔드 구현 전에 프론트엔드는 스키마 기반으로 mock 데이터를 만들어 개발을 시작합니다.
- 병렬 개발: 백엔드와 프론트엔드가 동시에 개발하고, 완성되면 통합합니다.
- 자동 테스트: 스키마 기반으로 E2E 테스트를 작성합니다.
# 개발 초기 단계: 구현 전에 스키마와 엔드포인트만 정의
@router.post("/posts", response=PostSchema)
def create_post(request, payload: PostCreateSchema):
"""
TODO: 실제 구현 필요
현재는 mock 데이터를 반환합니다.
"""
return {
"id": 999,
"title": payload.title,
"content": payload.content,
"author": "mock_user",
"created_at": "2026-01-18T10:00:00Z",
"tags": payload.tags,
"is_published": False
}
4. 문서 스타일 커스터마이징
Swagger UI의 스타일을 커스터마이징할 수 있습니다:
api = NinjaAPI(
title="My API",
version="1.0.0",
docs_url="/docs",
openapi_extra={
"info": {
"x-logo": {
"url": "https://example.com/logo.png",
"altText": "My Company Logo"
},
"contact": {
"name": "Backend Team",
"email": "backend@example.com",
"url": "https://example.com/support"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
}
}
)
5. 에러 처리 표준화하기
모든 에러를 일관된 형식으로 반환하면 프론트엔드에서 처리하기 쉽습니다:
from ninja import NinjaAPI
from ninja.errors import ValidationError
class APIError(Schema):
error: str
detail: str
code: str
@api.exception_handler(ValidationError)
def validation_error_handler(request, exc):
return api.create_response(
request,
{"error": "Validation Error", "detail": str(exc), "code": "VALIDATION_ERROR"},
status=400,
)
@api.exception_handler(Exception)
def general_exception_handler(request, exc):
return api.create_response(
request,
{"error": "Internal Server Error", "detail": "An unexpected error occurred", "code": "INTERNAL_ERROR"},
status=500,
)
결론
Django-Ninja의 자동 문서화 기능은 단순히 문서를 생성하는 것을 넘어, 프론트엔드와 백엔드 간의 계약(Contract)을 코드로 정의하는 것입니다. 코드가 곧 문서이므로 문서가 오래되거나 잘못될 일이 없고, 타입 안정성을 확보할 수 있으며, 협업 비용을 크게 줄일 수 있습니다.
핵심은 다음과 같습니다:
- 명확한 스키마 설계: Field 설명, 예시, 검증 규칙을 충실히 작성하세요.
- 일관된 패턴: 페이지네이션, 에러 응답, 인증 등을 일관되게 유지하세요.
- 프론트엔드 관점: TypeScript 타입 생성, 환경별 URL, 변경 이력 등을 제공하세요.
- 지속적인 개선: CI/CD에 문서 검증을 추가하고, 프론트엔드 피드백을 반영하세요.
API 문서화는 한 번 설정하면 끝나는 것이 아니라, 프로젝트 전체의 개발 경험을 향상시키는 지속적인 투자입니다. Django-Ninja를 활용하면 이 투자를 최소한의 노력으로 최대한의 효과를 낼 수 있습니다.
프론트엔드 개발자로부터 “API 문서가 정말 잘 되어 있어서 개발이 편했어요”라는 말을 들을 수 있을 것입니다. 그것이야말로 좋은 백엔드 시스템의 증거입니다.
참고 자료: