장고 실전 #1 DRF 시작 — Serializer, ViewSet, Router
장고 기초 → 중급 → 고급 21편의 도구가 한곳에 모이는 시리즈입니다. 풀스택 Django가 템플릿/폼/세션을 그대로 쓴다면, **DRF (Django REST Framework)**는 그 같은 ORM,인증 기반 위에 REST API 레이어를 깔끔하게 얹어줍니다.
- #1 DRF 시작 — Serializer, ViewSet, Router ← 이번 글
- #2 인증/권한 — Token, JWT, custom permission
- #3 Filtering / Ordering / Pagination
- #4 Celery로 비동기 작업
- #5 OpenAPI 문서 (drf-spectacular)
- #6 테스트와 배포 — Docker, gunicorn, nginx
만드는 것은 작은 블로그 API — Post, Comment, Author도메인. 글 CRUD, 댓글, 인증, 검색/페이지네이션, 무거운 작업은 Celery, 스키마 자동 문서, 마지막에 컨테이너로 배포까지 정리합니다. 매 글이 그 위에 한 단계씩 쌓아 올립니다.
DRF의 위치 — 풀스택 Django와 어떻게 다른가 #
기초 #4 ~ #6에서 본 풀스택 Django는 템플릿 + 폼 + 서버 렌더링 흐름이었습니다. 사용자가 브라우저에서 양식을 제출하면 view가 HttpResponse(render(...))로 HTML을 돌려주는 구조였습니다.
요즘 SPA(React/Vue/Next) + 모바일 앱 조합에서는 서버가 JSON만 돌려주고, 화면은 클라이언트가 그립니다. 이 자리에 들어오는 게 DRF입니다.
| 풀스택 Django | Django + DRF | |
|---|---|---|
| 응답 | HTML (template) | JSON |
| 입력 | Form / ModelForm | Serializer |
| 인증 | session + cookie | Token / JWT (+ session) |
| 클라이언트 | 브라우저 (서버 렌더링) | SPA / 모바일 / 외부 |
| 자동 문서 | 별도 | drf-spectacular (#5) |
| 학습 곡선 | 평탄 | 평탄 + DRF 추가 개념 |
핵심은 Django ORM, auth, admin, signals 같은 기반은 그대로 가져온다는 점입니다. DRF는 그 위에 입력/출력을 JSON으로 바꾸는 얇은 레이어입니다.
FastAPI와 비교 #
같은 용도(JSON API)에 FastAPI도 있습니다. 비교 표는 이 정도.
| Django + DRF | FastAPI | |
|---|---|---|
| 스타일 | 풀스택 위에 API 레이어 | 마이크로 + 타입 |
| ORM | Django ORM (성숙) | 외부 (SQLAlchemy 등) |
| 인증/권한 | 빌트인 (auth, permissions) | 직접 조립 |
| Admin | 빌트인 강력 | 없음 (외부) |
| 마이그레이션 | makemigrations/migrate | Alembic (외부) |
| 자동 문서 | drf-spectacular (별도) | 빌트인 |
| 비동기 | 부분 (4.0+) | 네이티브 |
| 데이터 검증 | Serializer | Pydantic |
| 어울리는 경우 | 운영 중심, admin/사용자/권한이 많은 서비스 | 마이크로서비스, 데이터/ML API |
이미 Django로 만든 모놀리식 서비스라면 DRF가 자연스럽고, 처음부터 작은 비동기 API만 있으면 FastAPI가 가볍습니다. 이 시리즈는 DRF 쪽을 깊게 봅니다.
프로젝트 셋업 #
기초 #2의 셋업 흐름 위에서 DRF를 추가합니다.
uv init blog-api --python 3.13
cd blog-api
uv add django djangorestframework
uv run django-admin startproject mysite .
uv run python manage.py startapp blogINSTALLED_APPS 등록
#
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework", # DRF 본체
"blog", # 우리 앱
]DRF는 단순히 앱 하나로 추가됩니다. 별도 미들웨어 변경 없이 바로 동작.
도메인 모델 #
from django.conf import settings
from django.db import models
class Post(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
title = models.CharField(max_length=200)
body = models.TextField()
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="comments",
)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created_at"]기초 #3에서 본 Model 정의 그대로입니다. DRF는 Django 모델을 그대로 쓰는 게 핵심.
uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py createsuperuserSerializer — DRF의 입력/출력 스키마 #
Serializer는 Python 객체 ↔ JSON 변환 + 검증을 담당합니다. Django의 Form/ModelForm과 구조가 비슷한데, HTML이 아닌 JSON을 다룬다는 차이가 핵심.
가장 기본 — serializers.Serializer
#
from rest_framework import serializers
class PostSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
body = serializers.CharField()
published = serializers.BooleanField(default=False)
created_at = serializers.DateTimeField(read_only=True)
def create(self, validated_data):
return Post.objects.create(**validated_data)
def update(self, instance, validated_data):
for k, v in validated_data.items():
setattr(instance, k, v)
instance.save()
return instance필드 하나하나를 직접 적는 구조입니다. read_only=True 인 필드는 출력에만 들어갑니다 (서버가 만드는 id, created_at).
ModelSerializer — 모델에서 자동 추론
#
대부분은 모델을 그대로 직렬화합니다. ModelSerializer 한 줄이면 끝.
from rest_framework import serializers
from .models import Post, Comment
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "author", "title", "body", "published", "created_at", "updated_at"]
read_only_fields = ["id", "author", "created_at", "updated_at"]
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "post", "author", "body", "created_at"]
read_only_fields = ["id", "author", "created_at"]ModelSerializer가 자동으로 해주는 것:
- 모델 필드에서 serializer 필드 추론 (
CharField→serializers.CharField) create(),update()기본 구현 제공- 모델의
unique=True,max_length등 검증 자동 반영
fields = "__all__"로 모든 필드를 노출할 수도 있지만, 명시적으로 적는 게 안전합니다 — 새 필드가 모델에 추가되면 의도치 않게 노출될 수 있습니다.
read_only_fields의 의미
#
author를 read-only로 둔 이유: 클라이언트가 다른 사람의 글로 위조하지 못하게 막기 위함입니다. ViewSet의 perform_create에서 request.user를 author로 넣어 주는 패턴을 #2에서 봅니다.
검증 — validate_<field>, validate, custom validator
#
Serializer에 검증을 얹는 지점은 세 군데입니다.
1) 필드 단위 — validate_<field>
#
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "title", "body", "published"]
def validate_title(self, value: str) -> str:
if value.strip().lower() in {"untitled", "test"}:
raise serializers.ValidationError("의미 없는 제목 금지")
return value.strip()validate_<필드명> 메소드는 그 필드 값만 받아서 검증합니다.
2) 모델 단위 — validate
#
def validate(self, attrs: dict) -> dict:
if attrs.get("published") and len(attrs.get("body", "")) < 100:
raise serializers.ValidationError(
{"body": "공개 글은 최소 100자 이상이어야 합니다."}
)
return attrs여러 필드를 같이 검사할 때. attrs가 모든 검증된 값의 dict.
3) 재사용 가능한 validator #
def no_html_tags(value: str) -> None:
if "<script" in value.lower():
raise serializers.ValidationError("script 태그는 허용되지 않음")
class PostSerializer(serializers.ModelSerializer):
body = serializers.CharField(validators=[no_html_tags])
...함수형 validator는 여러 serializer에서 재사용하기 좋습니다.
검증 실패 시 자동으로 400 Bad Request + 어디가 문제인지 JSON으로 응답:
{
"title": ["의미 없는 제목 금지"],
"body": ["공개 글은 최소 100자 이상이어야 합니다."]
}View의 계층 — APIView → Generic → ViewSet #
DRF의 view는 계층 입니다. 아래로 갈수록 짧고, 위로 갈수록 자유롭습니다.
APIView — 가장 원시적, get/post/put 직접 구현
↓
GenericAPIView — queryset / serializer_class 같은 공통 속성
↓ (+ mixins)
ListAPIView, RetrieveAPIView, CreateAPIView, ...
↓ (+ Router)
ViewSet → ModelViewSet — CRUD 한 줄1) APIView — 가장 원시적 #
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class PostListAPIView(APIView):
def get(self, request):
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
return Response(serializer.data)
def post(self, request):
serializer = PostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)Django의 CBV와 닮은 구조 — get(), post() 메소드를 직접 구현. 가장 명시적이지만 같은 코드가 반복됩니다.
2) GenericAPIView + mixins — 공통 패턴 추출 #
from rest_framework import generics, mixins
class PostListCreateAPIView(
mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView,
):
queryset = Post.objects.all()
serializer_class = PostSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)queryset + serializer_class 두 줄이 핵심. mixins가 list(), create() 같은 메소드를 제공.
3) Generic 단축형 — ListCreateAPIView 등
#
from rest_framework import generics
class PostListCreateAPIView(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializerDRF가 자주 쓰는 조합을 미리 만들어 둔 클래스들.
| 클래스 | 메소드 |
|---|---|
ListAPIView | GET (list) |
CreateAPIView | POST |
ListCreateAPIView | GET + POST |
RetrieveAPIView | GET (detail) |
UpdateAPIView | PUT/PATCH |
DestroyAPIView | DELETE |
RetrieveUpdateDestroyAPIView | GET + PUT/PATCH + DELETE |
목록과 상세를 두 클래스로 나눠 쓰고, URL도 둘로 분리합니다.
ViewSet — 한 클래스로 CRUD 전체 #
ViewSet은 한 클래스에 list/retrieve/create/update/destroy를 다 묶고, Router가 URL을 자동으로 만들어 줍니다. DRF의 표준 패턴.
from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)이 4 줄이 다음을 다 만듭니다:
| 메소드 | 메소드 (HTTP) | URL | 동작 |
|---|---|---|---|
list | GET | /posts/ | 목록 |
retrieve | GET | /posts/{id}/ | 상세 |
create | POST | /posts/ | 생성 |
update | PUT | /posts/{id}/ | 전체 수정 |
partial_update | PATCH | /posts/{id}/ | 부분 수정 |
destroy | DELETE | /posts/{id}/ | 삭제 |
perform_create()는 save 시점에 추가 데이터를 넣을 때 쓰는 훅입니다. 위에서는 author를 현재 요청 사용자로 넣어 줍니다.
그 외 자주 쓰는 훅 #
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
def get_queryset(self):
# 동적으로 queryset 결정 — 예: 자기 글만
qs = super().get_queryset()
if self.action == "list" and self.request.user.is_authenticated:
return qs.filter(author=self.request.user)
return qs.filter(published=True)
def get_serializer_class(self):
# 액션마다 다른 serializer
if self.action == "list":
return PostListSerializer
return PostSerializerget_queryset()— 액션/사용자/권한별로 동적 querysetget_serializer_class()— list는 가벼운 직렬화, retrieve/create는 자세한 직렬화 같은 분리
@action — 추가 엔드포인트
#
표준 CRUD 외에 액션을 더하고 싶을 때.
from rest_framework.decorators import action
from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
"""POST /posts/{id}/publish/ — 글 공개 처리."""
post = self.get_object()
post.published = True
post.save()
return Response({"published": True})
@action(detail=False, methods=["get"])
def drafts(self, request):
"""GET /posts/drafts/ — 내 초안 목록."""
qs = self.get_queryset().filter(author=request.user, published=False)
serializer = self.get_serializer(qs, many=True)
return Response(serializer.data)detail=True— 인스턴스 단위 (/posts/{id}/publish/)detail=False— 컬렉션 단위 (/posts/drafts/)
Router — URL 자동 생성 #
ViewSet은 그 자체로 URL에 등록할 수 없습니다 — 어떤 메소드를 어떤 HTTP 동사에 매핑할지 알려줘야 합니다. Router가 그걸 자동으로 합니다.
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CommentViewSet
router = DefaultRouter()
router.register(r"posts", PostViewSet, basename="post")
router.register(r"comments", CommentViewSet, basename="comment")
urlpatterns = router.urlsfrom django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("blog.urls")),
]이게 끝입니다. DefaultRouter가 만들어주는 URL:
GET /api/posts/ → list
POST /api/posts/ → create
GET /api/posts/{pk}/ → retrieve
PUT /api/posts/{pk}/ → update
PATCH /api/posts/{pk}/ → partial_update
DELETE /api/posts/{pk}/ → destroy
POST /api/posts/{pk}/publish/ → @action(detail=True) publish
GET /api/posts/drafts/ → @action(detail=False) drafts
GET /api/ → API root (DefaultRouter만)DefaultRouter는 /api/ 루트에 API 인덱스도 자동으로 만들어 줍니다. SimpleRouter는 인덱스 없이 라우트만.
Browsable API — DRF의 비밀 무기 #
브라우저에서 /api/posts/를 그냥 열어 보세요. DRF가 HTML 인터페이스를 자동으로 만들어 줍니다 — 폼으로 POST를 보내고, 응답을 JSON으로 보고, 인증 상태를 토글하는 UI까지 정리합니다.
API 개발 초기에 매우 유용합니다. 프로덕션에서 끄고 싶으면 DEFAULT_RENDERER_CLASSES에서 BrowsableAPIRenderer를 빼면 됩니다.
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
# "rest_framework.renderers.BrowsableAPIRenderer", # 개발에서만
],
}첫 결과물 — 한 화면에 #
from rest_framework import serializers
from .models import Post, Comment
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "author", "title", "body", "published", "created_at", "updated_at"]
read_only_fields = ["id", "author", "created_at", "updated_at"]
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "post", "author", "body", "created_at"]
read_only_fields = ["id", "author", "created_at"]from rest_framework import viewsets
from .models import Post, Comment
from .serializers import PostSerializer, CommentSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CommentViewSet
router = DefaultRouter()
router.register(r"posts", PostViewSet)
router.register(r"comments", CommentViewSet)
urlpatterns = router.urlsuv run python manage.py runserver
# → http://127.0.0.1:8000/api/브라우저로 /api/posts/를 열면 Browsable API가 나오고, 폼으로 글을 만들 수 있습니다. 다음 글들이 그 위에 인증/필터링/비동기/문서/배포를 한 단계씩 쌓아 올립니다.
정리 #
이번 글에서 잡은 것:
- DRF의 위치 — Django 위의 JSON API 레이어 (ORM/auth/admin 그대로)
- Django + DRF vs FastAPI 비교 — 어울리는 용도가 다름
INSTALLED_APPS에rest_framework한 줄 추가- Serializer — Python ↔ JSON + 검증
serializers.Serializer(수동),ModelSerializer(자동)read_only_fields로 서버 결정 필드 보호
- 검증의 세 지점 —
validate_<field>,validate, 함수형 validator - View 계층 —
APIView→GenericAPIView+ mixins → 단축형 →ViewSet ModelViewSet— 한 클래스로 CRUD,perform_create로 author 적기@action으로 추가 엔드포인트 (publish,drafts)- Router —
DefaultRouter().register()로 URL 자동 생성 - Browsable API — 브라우저에서 바로 테스트 가능한 HTML 인터페이스
회귀 링크: 기초 #3 Models와 ORM의 모델 정의가 그대로 들어왔고, 기초 #7 Admin과 인증의 User 모델이 author ForeignKey로 연결됐습니다.
다음 글(#2 인증/권한)에서는 누가 그 글을 쓰고 수정할 수 있는지 — Token/JWT 인증, IsAuthenticated/IsOwner 같은 permission 클래스를 다룹니다.