클래스 기반 뷰 입문

클래스 기반 뷰 입문 (Introduction to class-based views)

Django의 뷰는 처음에는 함수 하나로 작성하는 방식이었어요. 클래스 기반 뷰(Class-based views, CBV)는 뷰를 파이썬 객체로 표현하는 또 다른 방식이에요. 함수 기반 뷰를 대체하는 게 아니라, HTTP 메서드별로 코드를 나누고 믹스인(mixin)을 재사용할 수 있는 장점을 주는 대안이에요.

출처: Django 공식 문서 - Introduction to class-based views

언제 클래스 기반 뷰를 쓰는가

함수 기반 뷰에서는 GET, POST 같은 HTTP 메서드를 구분하려고 보통 if request.method == "GET" 식의 분기문을 써요. 클래스 기반 뷰에서는 메서드별 처리를 별도의 인스턴스 메서드로 나눌 수 있어요. 그리고 여러 부모 클래스의 동작을 조합하는 믹스인(다중 상속)을 활용해 코드를 재사용 가능한 부품으로 쪼갤 수 있어요.

뷰의 변천: 제네릭 뷰와 클래스 기반 제네릭 뷰

Django는 초기에 뷰 함수에 HttpRequest를 넘기고 HttpResponse를 돌려받는 계약만 제공했어요. 그러다 뷰 개발에서 자주 반복되는 패턴을 추상화한 "함수 기반 제네릭 뷰"가 등장했지요. 그런데 이들은 단순한 경우는 잘 다뤄 주지만, 구성 옵션 몇 개를 바꾸는 것 이상으로 확장하거나 커스터마이즈할 방법이 없어서 실제 애플리케이션에서는 한계가 있었어요.

클래스 기반 제네릭 뷰는 같은 목표를 믹스인으로 구현해서, 함수 기반 제네릭 뷰보다 훨씬 확장성 있고 유연한 도구 모음(toolkit)을 만들어 냈어요. 과거에 함수 기반 제네릭 뷰가 부족하다고 느꼈다면, 클래스 기반 제네릭 뷰를 단순한 "클래스 버전"으로 보지 말고, 제네릭 뷰가 원래 풀려고 했던 문제를 푸는 새로운 접근으로 보는 게 좋아요.

이 도구 모음은 최대한의 유연성을 위해 만들어져서 여러 후크(hook)를 갖고 있어요. 예를 들어 form_class라는 클래스 속성 하나로 제한하는 대신, get_form() 메서드가 get_form_class() 메서드를 호출하고, 그 기본 구현이 form_class 속성을 돌려주는 구조예요. 단순한 상황에서는 다소 과해 보일 수 있지만, 이 옵션들이 있어야 더 고급스러운 설계가 가능해져요.

클래스 기반 뷰 사용하기

클래스 기반 뷰의 핵심은 "HTTP 요청 메서드마다 다른 인스턴스 메서드를 호출"한다는 거예요. 함수 기반 뷰에서 GET 처리가 다음과 같다면,

from django.http import HttpResponse

def my_view(request):
    if request.method == "GET":
        # <view logic>
        return HttpResponse("result")

클래스 기반 뷰에서는 이렇게 돼요.

from django.http import HttpResponse
from django.views import View

class MyView(View):
    def get(self, request):
        # <view logic>
        return HttpResponse("result")

Django의 URL 리졸버는 클래스가 아니라 호출 가능한(callable) 함수를 기대하기 때문에, 클래스 기반 뷰에는 as_view()라는 클래스 메서드가 있어요. 이 메서드는 요청이 들어왔을 때 호출할 함수를 돌려줘요. 그 함수는 클래스 인스턴스를 만들고, setup()으로 속성을 초기화한 뒤 dispatch()를 호출해요. dispatch()는 요청이 GET인지 POST인지 등을 판별해서 일치하는 메서드로 넘기고, 없으면 HttpResponseNotAllowed를 일으켜요.

# urls.py
from django.urls import path
from myapp.views import MyView

urlpatterns = [
    path("about/", MyView.as_view()),
]

메서드가 반환하는 것은 함수 기반 뷰와 똑같이 HttpResponse 계열이에요. 그래서 render() 같은 http shortcut이나 TemplateResponse 객체를 클래스 기반 뷰 안에서 그대로 쓸 수 있어요.

클래스 속성 구성하기

클래스 기반 뷰의 동작을 바꾸는 방법은 두 가지가 있어요. 첫째는 파이썬답게 서브클래스를 만들어 속성과 메서드를 오버라이드하는 것이에요.

from django.http import HttpResponse
from django.views import View

class GreetingView(View):
    greeting = "Good Day"

    def get(self, request):
        return HttpResponse(self.greeting)

class MorningGreetingView(GreetingView):
    greeting = "Morning to ya"

둘째는 as_view() 호출에 클래스 속성을 키워드 인자로 넘기는 방법이에요.

urlpatterns = [
    path("about/", GreetingView.as_view(greeting="G'day")),
]

참고로, 클래스는 요청마다 새로 인스턴스화되지만 as_view()로 지정한 클래스 속성은 URL이 import되는 시점에 단 한 번만 구성돼요.

믹스인 사용하기

믹스인은 여러 부모 클래스의 동작과 속성을 조합하는 다중 상속 형태예요. 예를 들어 제네릭 클래스 기반 뷰에는 render_to_response() 메서드를 정의하는 TemplateResponseMixin이라는 믹스인이 있어요. 이 믹스인이 View 기본 클래스의 동작과 결합하면, 요청을 알맞은 메서드로 분배하면서 template_name 속성으로 TemplateResponse를 돌려주는 TemplateView가 만들어져요.

믹스인은 여러 클래스에서 코드를 재사용하기 좋지만 대가도 있어요. 코드가 믹스인들에 흩어질수록 자식 클래스가 정확히 무엇을 하는지 읽기가 어려워지고, 깊은 상속 트리에서 어떤 믹스인의 어떤 메서드를 오버라이드해야 할지 파악하기도 힘들어져요.

주의할 점도 있어요. 제네릭 뷰는 View에서 물려받는 부모가 하나여야 해요. 나머지는 전부 믹스인이어야 해요. View를 상속받는 클래스를 두 개 이상 상속하려 하면 예상대로 동작하지 않아요.

클래스 기반 뷰로 폼 다루기

함수 기반 뷰에서 폼을 처리하는 코드는 보통 이렇게 생겼어요.

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import MyForm

def myview(request):
    if request.method == "POST":
        form = MyForm(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect("/success/")
    else:
        form = MyForm(initial={"key": "value"})

    return render(request, "form_template.html", {"form": form})

클래스 기반 뷰로는 이렇게 만들 수 있어요.

from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views import View
from .forms import MyForm

class MyFormView(View):
    form_class = MyForm
    initial = {"key": "value"}
    template_name = "form_template.html"

    def get(self, request, *args, **kwargs):
        form = self.form_class(initial=self.initial)
        return render(request, self.template_name, {"form": form})

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect("/success/")
        return render(request, self.template_name, {"form": form})

아주 단순한 예시이지만, 이렇게 하면 form_class 같은 클래스 속성을 URLconf 설정이나 서브클래싱으로 오버라이드해 뷰를 유연하게 커스터마이즈할 수 있어요.

더 알아보기 (Learn more)