장고 중급 #1 Class-Based Views 깊이

6 분 소요

장고 기초 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 같은 흔한 패턴을 여러 모델에 반복해서 쓰다 보면 비슷한 코드가 계속 늘어납니다.

FBVCBV
정의함수 한 개클래스 + 메소드
학습 곡선매우 낮음약간 높음
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 메소드를 정의합니다.

blog/views.py — 가장 기본
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")
blog/urls.py — as_view()
from django.urls import path
from .views import HelloView

urlpatterns = [
    path('hello/', HelloView.as_view(), name='hello'),
]

HelloView.as_view()클래스 → 호출 가능한 뷰 함수로 변환합니다. URL 패턴은 항상 호출 가능한 객체를 받기 때문입니다.

dispatch — 모든 요청의 진입점 #

CBV는 모든 메소드 분기 전에 **dispatch**가 한 번 호출됩니다. 공통 처리(인증, 로깅 등)를 여기에 넣을 수 있습니다.

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

TemplateViewRedirectView #

가장 짧은 CBV 두 개를 먼저 익히면 패턴이 잡힙니다.

TemplateView — 정적 페이지
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 ctx
RedirectView — 리다이렉트만
from 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로 다시 풀어봅니다.

🚫 FBV — 직접 작성
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})
✅ CBV — ListView
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 qs

DetailView — 단건 조회 #

DetailView
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 ctx
urls.py — pk 또는 slug
urlpatterns = [
    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 패턴이 그대로 자동화됩니다.

CreateView
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)
UpdateView
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})
DeleteView
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 — 모델 없는 폼 #

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면 PermissionDenied

permission_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.user

test_funcTrue를 돌려주면 통과. 객체 단위 권한 (이 글의 저자만 수정) 같은 경우에 깔끔합니다.

직접 만드는 Mixin #

공통 컨텍스트 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의 흐름을 한 번 따라가 봅니다.

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 등록 패턴 #

blog/urls.py — CBV 등록
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 CBVListView, DetailView, CreateView, UpdateView, DeleteView, FormView
  • 핵심 오버라이드: get_queryset, get_context_data, form_valid, get_success_url
  • MixinLoginRequiredMixin, 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 문제와 그 해결까지 한 호흡에 다루겠습니다.

X