장고 중급 #1 Class-Based Views 깊이
장고 기초 7편을 마쳤다면, 이제 한 단계 위로 들어갑니다. 중급 시리즈는 기초에서 살짝 비춘 도구들을 본격적으로 다루는 7편입니다.
- #1 Class-Based Views 깊이 ← 이번 글
- #2 ORM 중급 — annotate, aggregate, F/Q, prefetch_related
- #3 Signals와 Middleware
- #4 사용자/권한 — 커스텀 user model
- #5 메시지 / 세션 / 쿠키
- #6 Static/Media 운영과 storage backends
- #7 테스트 — TestCase, fixtures, pytest-django
첫 주제는 Class-Based Views (CBV) 입니다. 기초 #4에서 함수형 뷰 (FBV)로 만든 코드를, 재사용 가능한 클래스로 다시 풀어내는 글입니다.
FBV vs CBV — 왜 클래스인가 #
기초 #4에서 본 함수형 뷰는 직관적입니다. 요청을 받아 응답을 돌려주는 함수 한 개. 그런데 CRUD 같은 흔한 패턴을 여러 모델에 반복해서 쓰다 보면 비슷한 코드가 계속 늘어납니다.
| FBV | CBV | |
|---|---|---|
| 정의 | 함수 한 개 | 클래스 + 메소드 |
| 학습 곡선 | 매우 낮음 | 약간 높음 |
| HTTP 메소드 분기 | if request.method == 'POST' | def get, def post |
| 재사용 | 데코레이터 / 함수 분해 | 상속 / Mixin |
| Generic CRUD | 직접 구현 | 빌트인 (ListView 등) |
| 코드 흐름 추적 | 위에서 아래로 명확 | 부모 클래스를 따라 추적 필요 |
CBV의 강점은 흔한 패턴을 부모 클래스에 두고 차이점만 오버라이드하는 점입니다. 단점은 흐름이 부모 메소드를 거쳐가서 처음엔 어디가 어떻게 동작하는지 추적이 어렵다는 점입니다.
규칙 하나: 단순 라우트 (헬스체크, 단순 리다이렉트, 작은 폼)는 FBV, CRUD / 리스트 / 디테일 같은 정형 패턴은 CBV가 보통 답입니다. 어느 한쪽으로 통일할 필요 없습니다.
CBV의 출발점 — View
#
가장 기본 클래스는 django.views.View입니다. HTTP 메소드별로 get, post, put, delete 메소드를 정의합니다.
from django.http import HttpResponse
from django.views import View
class HelloView(View):
def get(self, request):
return HttpResponse("Hello, GET")
def post(self, request):
return HttpResponse("Hello, POST")from django.urls import path
from .views import HelloView
urlpatterns = [
path('hello/', HelloView.as_view(), name='hello'),
]HelloView.as_view()가 클래스 → 호출 가능한 뷰 함수로 변환합니다. URL 패턴은 항상 호출 가능한 객체를 받기 때문입니다.
dispatch — 모든 요청의 진입점
#
CBV는 모든 메소드 분기 전에 **dispatch**가 한 번 호출됩니다. 공통 처리(인증, 로깅 등)를 여기에 넣을 수 있습니다.
class HelloView(View):
def dispatch(self, request, *args, **kwargs):
print(f"메소드: {request.method}")
return super().dispatch(request, *args, **kwargs)
def get(self, request):
return HttpResponse("GET")TemplateView와 RedirectView
#
가장 짧은 CBV 두 개를 먼저 익히면 패턴이 잡힙니다.
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = "blog/about.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["page_title"] = "회사 소개"
return ctxfrom django.views.generic import RedirectView
class GoToBlogView(RedirectView):
pattern_name = "post_list" # url name 지정
permanent = False
query_string = True # 쿼리스트링 이어붙이기get_context_data가 템플릿 컨텍스트를 만드는 메소드입니다. 거의 모든 CBV가 이 메소드를 가지고 있습니다.
Generic CBV — CRUD의 빌트인 #
여기부터가 CBV의 본 매력입니다. 장고가 흔한 CRUD 패턴을 미리 만들어 둔 클래스들입니다.
ListView — 목록 페이지
#
기초 #4에서 함수형으로 적은 글 목록을, CBV로 다시 풀어봅니다.
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.filter(published=True).order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
ordering = ["-created_at"]
def get_queryset(self):
return super().get_queryset().filter(published=True)핵심 클래스 속성:
model— 어떤 모델의 목록인지template_name— 안 적으면<app>/<model>_list.html자동 추정context_object_name— 템플릿에서 받을 변수명 (기본object_list)paginate_by— 자동 페이지네이션 (장고가page_obj,paginator컨텍스트 같이 넘김)ordering— 기본 정렬
get_queryset을 오버라이드해서 추가 필터를 겁니다. URL 쿼리스트링으로 검색 조건을 받을 때도 이 지점에서 풀면 깔끔합니다.
class PostListView(ListView):
model = Post
paginate_by = 10
def get_queryset(self):
qs = super().get_queryset().filter(published=True)
q = self.request.GET.get("q")
if q:
qs = qs.filter(title__icontains=q)
return qsDetailView — 단건 조회
#
from django.views.generic import DetailView
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
context_object_name = "post"
slug_field = "slug"
slug_url_kwarg = "slug"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["related"] = Post.objects.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:5]
return ctxurlpatterns = [
path('posts/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
# 또는 slug 기반
path('posts/<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]DetailView는 URL의 pk 또는 slug로 객체를 자동 조회합니다. 못 찾으면 자동으로 404.
CreateView, UpdateView, DeleteView
#
기초 #6의 ModelForm 패턴이 그대로 자동화됩니다.
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .forms import PostForm
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
success_url = reverse_lazy("post_list")
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
def get_success_url(self):
return reverse_lazy("post_detail", kwargs={"slug": self.object.slug})class PostDeleteView(DeleteView):
model = Post
template_name = "blog/post_confirm_delete.html"
success_url = reverse_lazy("post_list")핵심 오버라이드 포인트:
form_valid(form)— 폼이 통과한 직후. 저자 자동 지정, 슬러그 자동 생성 등form_invalid(form)— 검증 실패 시get_success_url()— 성공 후 리다이렉트 URL (동적이면 메소드, 정적이면success_url속성)get_form_kwargs()— 폼 생성자에 추가 인자 넘기기
reverse_lazy를 쓰는 이유는 클래스 정의 시점엔 URL conf가 아직 로드 안 됐을 수 있어서입니다. 호출 시점까지 reverse를 미루는 것이 lazy의 의미입니다.
FormView — 모델 없는 폼
#
from django.views.generic.edit import FormView
from .forms import ContactForm
class ContactView(FormView):
template_name = "blog/contact.html"
form_class = ContactForm
success_url = reverse_lazy("contact_done")
def form_valid(self, form):
form.send_email()
return super().form_valid(form)DB 저장 없이 폼 검증 + 후처리만 필요한 경우에 어울립니다.
Mixin — 작은 동작들의 조립 #
CBV의 진짜 힘은 Mixin 입니다. 작은 동작 (인증 체크, 권한 체크, 컨텍스트 추가 등)을 별도 클래스로 두고 다중 상속으로 조립합니다.
LoginRequiredMixin — 로그인 필수
#
from django.contrib.auth.mixins import LoginRequiredMixin
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
login_url = "/accounts/login/" # 미로그인시 보낼 곳 (기본 settings.LOGIN_URL)
redirect_field_name = "next"LoginRequiredMixin이 첫 번째에 와야 합니다. 다중 상속의 MRO 때문에 검사 로직이 먼저 실행되도록.
PermissionRequiredMixin — 권한 검사
#
from django.contrib.auth.mixins import PermissionRequiredMixin
class PostUpdateView(PermissionRequiredMixin, UpdateView):
model = Post
form_class = PostForm
permission_required = "blog.change_post"
raise_exception = True # 없으면 403, True면 PermissionDeniedpermission_required는 문자열 또는 리스트. 권한 시스템 자체는 #4 사용자/권한에서 자세히.
UserPassesTestMixin — 임의 조건
#
from django.contrib.auth.mixins import UserPassesTestMixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
def test_func(self):
post = self.get_object()
return post.author == self.request.usertest_func가 True를 돌려주면 통과. 객체 단위 권한 (이 글의 저자만 수정) 같은 경우에 깔끔합니다.
직접 만드는 Mixin #
class SidebarContextMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["recent_posts"] = Post.objects.order_by("-created_at")[:5]
ctx["popular_tags"] = Tag.objects.popular()[:10]
return ctx
class PostListView(SidebarContextMixin, ListView):
model = Post
class PostDetailView(SidebarContextMixin, DetailView):
model = Post여러 뷰에서 공통으로 쓰는 컨텍스트를 한곳에 모읍니다. 클래스 상속의 본 효용이 살아나는 지점입니다.
흐름 추적 — 어디가 어떻게 동작하는가 #
CBV가 어렵게 느껴지는 가장 큰 이유는 부모 클래스의 메소드 흐름을 모르기 때문입니다. ListView의 흐름을 한 번 따라가 봅니다.
URL → as_view() → dispatch(request)
↓
get(request)
↓
self.object_list = self.get_queryset()
↓
context = self.get_context_data()
↓
self.render_to_response(context)오버라이드할 메소드:
| 메소드 | 쓰임 |
|---|---|
dispatch | 모든 메소드 공통 전처리 |
get_queryset | 목록의 쿼리셋 가공 (필터, 정렬) |
get_context_data | 템플릿에 전달할 추가 데이터 |
get_template_names | 템플릿 이름을 동적으로 결정 |
form_valid/form_invalid | 폼 처리 후크 (Create/Update) |
get_success_url | 성공 후 이동지 |
흐름이 헷갈리면 공식 문서의 Class-based generic views 페이지나 Classy CBV (ccbv.co.uk) 사이트가 큰 도움이 됩니다. 각 클래스의 모든 속성,메소드가 한눈에 정리돼 있습니다.
URL 등록 패턴 #
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path('posts/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
path('posts/new/', views.PostCreateView.as_view(), name='post_create'),
path('posts/<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post_update'),
path('posts/<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]as_view()는 클래스 메소드라서 인스턴스를 만들지 않고 호출합니다. URL 매칭 시점마다 새 인스턴스가 만들어져요 (요청 사이에 인스턴스 상태가 공유되지 않습니다).
언제 FBV / 언제 CBV #
| 상황 | 추천 |
|---|---|
| 헬스체크, 단순 리다이렉트, 간단한 검색 | FBV |
| API 한두 개로 끝나는 작은 엔드포인트 | FBV |
| 표준 CRUD (목록/상세/생성/수정/삭제) | CBV (Generic) |
| 권한,로그인 같은 가로지르는 관심사 반복 | CBV (Mixin) |
| 흐름이 복잡하고 분기가 많음 | FBV (가독성) |
| 여러 모델에 같은 패턴 반복 | CBV (재사용) |
장고 진영의 보통 답: 두 가지를 섞어 쓰는 게 자연스럽다. CBV가 좋은 경우는 CBV로, FBV가 좋은 경우는 FBV로 쓰면 됩니다.
정리 #
이번 글에서 잡은 것:
- CBV의 출발점 —
View+as_view()+ 메소드별 분기 dispatch가 모든 요청의 진입점TemplateView,RedirectView— 가장 짧은 CBV- Generic CBV —
ListView,DetailView,CreateView,UpdateView,DeleteView,FormView - 핵심 오버라이드:
get_queryset,get_context_data,form_valid,get_success_url - Mixin —
LoginRequiredMixin,PermissionRequiredMixin,UserPassesTestMixin, 직접 만든 컨텍스트 Mixin - Mixin은 상속 순서가 중요 (앞에 둔 게 먼저)
reverse_lazy는 클래스 정의 시점에 URL 해석을 미루는 도구- FBV/CBV는 양자택일 X — 경우에 맞게 섞어 쓰기
다음 글(#2 ORM 중급)에서는 기초 #3의 단순 QuerySet 위에 annotate, aggregate, F/Q, select_related/prefetch_related 같은 본격 ORM 도구를 쌓습니다. N+1 문제와 그 해결까지 한 호흡에 다루겠습니다.