[Two Scoops of Django] 10장. 클래스 기반 뷰의 모범적인 이용

guava·2021년 10월 23일
0

Two Scoops of Django

목록 보기
10/12
post-thumbnail

Two Scoops of Django 3.x를 보고 정리한 글입니다.

  • 장고의 뷰는 요청 객체(HttpRequest)를 받고 응답 객체(HttpResponse)를 반환하는 내장 함수다.
  • 함수 기반 뷰에서는 뷰 함수 자체가 내장 함수이다.
  • 클래스 기반 뷰는 뷰 클래스가 내장 함수를 반환하는 as_view() 클래스 메서드를 제공한다.
  • 이러한 클래스 기반 뷰의 매커니즘은 django.views.generic.View 클래스에서 구현된다.
  • 장고는 GCBV(generic class-based view)를 제공하며 이를 통해 웹 프로젝트의 공통 패턴을 제공한다.

10.1 클래스 기반 뷰를 이용할 때의 가이드라인

  • 뷰 코드가 적을수록 좋다.
  • 뷰 안에서 같은 코드를 반복하지 말라.
  • 뷰는 프레젠테이션 로직을 처리해야 한다. 비즈니스 로직은 모델에서 처리하거나 필요할 경우 폼에서 처리한다.
  • 뷰는 간단 명료해야 한다.
  • 믹스인은 간단 명료해야 한다.
  • CBV를 사용할 경우 ccbv 문서에 익숙해지자. CBV에 대한 공식문서보다 더 유용하다.

10.2 CBV와 함께 믹스인 사용하기

  • 프로그래밍에서 믹스인이란 실체화된 클래스가 아닌 상속해 줄 기능을 제공하는 클래스를 의미한다.
  • 다중 상속을 해야 할 때 믹스인을 쓰면 클래스에 더 나은 기능과 역할을 제공할 수 있다.
  • 믹스인을 사용해서 뷰 클래스를 구성할 때 케네스 러브(Kenneth Love)가 제안한 상속에 관한 규칙을 따르도록 하자. (왼쪽에서 오른 쪽 순서로 처리하는 파이썬의 메서드 처리 순서에 기반을 둔 것)

케네스 러브의 상속에 관한 규칙

규칙 1 장고가 제공하는 기본 뷰 클래스는 항상 오른쪽으로 이동한다.
규칙 2 믹스인은 기본 뷰에서부터 왼쪽으로 이동한다.
규칙 3 믹스인은 다른 클래스에서 상속되어서는 안된다. 상속 체인을 간단하게 유지하라.

Example 10.1: 뷰에서 믹스인 사용하기

앞서 제안한 규칙을 적용한 예시이다. FruityFlavorView클래스의 상속 문법을 보자.
규칙1에 의해 TemplateView가 장고에서 제공하는 기본 클래스이기에 오른쪽으로 이동했다
규칙2에 의해 믹스인은 왼쪽으로 이동했다.

from django.views.generic import TemplateView # 장고가 제공하는 기본 뷰 클래스

class FreshFruitMixin:
    """믹스인 클래스"""

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs) 
        context["has_fresh_fruit"] = True
        return context

class FruityFlavorView(FreshFruitMixin, TemplateView):
    # 믹스인은 왼쪽에(규칙 1), 장고의 기본 제공 클래스는 오른쪽으로 이동했다. (규칙 2)
    template_name = "fruity_flavor.html"

10.3 어떤 작업에 어떤 GCBV를 이용해야 하는가?

  • 제네릭 클래스 기반 뷰(이하 GCBV)는 최대 8개의 슈퍼 클래스가 상속되기도 하는 복잡한 상속체인을 갖는다.
  • 어떤 뷰를 이용할 지, 어떤 뷰를 커스텀 할지 결정하는 것은 까다로울 수 있다.
  • 이러한 문제를 완화하기 위해 각 클래스 기반 뷰의 목적과 이름을 나열하는 표를 만들어보았다. (여기에 나열된 모든 View는 django.views.generic접두사가 붙는것으로 한정한다)
이름목적Two Scoops Example
View어디에서든 이용 가능한 기본 뷰'10.6 django.views.generic.View' 이용하기 참고
RedirectView사용자를 다른 URL로 리다이렉트'/log-in/'을 방문한 사용자를 '/login/'으로 보내기
TemplateView장고 HTML 템플릿을 보여줌사이트의 '/about/' 페이지
ListView객체의 목록을 보여줌아이스크림 맛 목록
DetailView객체를 보여줌아이스크림 맛에 대한 세부사항
FormView폼 전송(제출)사이트 연락처 또는 이메일 폼
CreateView객체를 만들 때새로운 아이스크림 맛을 만들 때
UpdateView객체를 업데이트 할 때기존 아이스크림을 맛을 업데이트
DeleteView객체를 삭제할 때기존 아이스크림 맛을 삭제
Generic date views일정 시간동안 발생한 객체를 보여줌블로그가 일반적으로 이를 이용한다. Two Scoops의 경우 맛이 추가된 시기에 대한 public history를 만들 수 있다

장고 클래스 기반 뷰/제네릭 클래스 기반 뷰의 이용에 대한 세가지 의견

저자는 아래 그룹에 대해 첫번째 그룹을 지지하지만 진짜 실전 해답은 없음을 밝힌다

그룹 1 제네릭 뷰의 모든 종류를 최대한 이용하자

  • 장고의 사용 이유는 개발 작업의 양을 최소화 하는데 그 목적이 있다
  • 작업양의 최소화를 위해 제네릭 뷰의 모든 뷰를 최대한 이용하자.
  • 저자는 이 방법을 지지하며 다수의 프로젝트에서 쉽게 개발, 유지 보수하는데 큰 성공을 거두었다.

그룹 2 심플하게 django.views.generic.View 하나로 모든 뷰를 다 처리하자

  • 장고의 기본 클래스 ㄱ비ㅏㄴ 뷰만으로 충분히 원하는 기능을 소화할 수 있다.
  • 진정한 클래스 기반 뷰는 모든 뷰가 제네릭 클래스 기반 뷰여야 한다.
  • 저자는 첫번째의 '제네릭 뷰의 모든 종류를 최대한 이용하자'는 리소스 기반 접근 방식이 실패한 작업들에 대해 이 방법이 효율적임을 발견했다.
  • 이 장에서 이러한 몇가지 경우에 대해 다룬다.

그룹 3 django.views.generic.View를 정말 상속할 것이 아닌 이상 그냥 무시하자.

  • 제이콥 캐플런모스의 조언: "읽기 쉽고 이해하기 쉬운 함수 기반 뷰로 시작하자. CBV가 필요한 상황이 되었을 때만 CBV를 사용하자. 그 경우는 여러 뷰에서 재사용할 수 있는 코드 양이 많아졌을 때다."

10.4 장고 CBV에 대한 일반적인 팁

  • 이 절에서 Class Based View(이하 CBV)와 Generic Class Based View(이하 GCBV) 구현에 대한 팁을 다룬다.
  • CBV, GCBV는 뷰, 템플릿, 테스트를 신속하게 구현하는데 목적이 있다.
  • 이 기법은 CBV, GCBV에서 작동한다.
  • 장고의 CBV는 객체지향 기술에 전적으로 의존한다.

10.4.1 인증된 사용자에게만 CBV/GCBV 접근 가능하게 하기

# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin 
from django.views.generic import DetailView

from .models import Flavor

class FlavorDetailView(LoginRequiredMixin, DetailView):
       model = Flavor

GCBV 믹스인 순서를 명심하자

  • LoginRequiredMixin은 항상 왼쪽에 위치한다.
  • 기본 뷰 클래스는 항상 오른쪽에 위치한다.

10.4.2 유효한 폼과 함께 커스텀 액션 수행하기

Example 10.3: Custom Logic with Valid Forms

  • 이미 검증된 폼 데이터에 대해 커스텀 로직을 추가하려면 form_valid()에 로직을 추가하라.
  • form_valid()는 django.http.HttpResponseRedirect이여야 한다.
from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import CreateView

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView): 
    model = Flavor
   fields = ['title', 'slug', 'scoops_remaining']
   
    def form_valid(self, form):
        # Do custom logic here
        return super().form_valid(form)

10.4.3 유효하지 않은 폼과 함께 커스텀 액션 수행하기

Example 10.4: Overwriting Behavior of form_invalid

  • invalid form이 있는 보기에서 커스텀 액션을 수행해야 할 때 form_invalid()메서드는 장고의 GCBV 워크플로가 요청을 보내는 곳이다.
  • 이 메서드는 django.http.HttpResponse를 반환해야 한다.
from django.contrib.auth.mixins import LoginRequiredMixin 
from django.views.generic import CreateView

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):
    model = Flavor
       
    def form_invalid(self, form):
        # Do custom logic here
        return super().form_invalid(form)

10.4.4 뷰 객체(View Object) 이용하기

  • 콘텐츠를 랜더링 하는데 CBV를 이용한다면 다음을 고려하자.
  • 메서드와 프로퍼티를 제공하기 위한 뷰 객체를 정의한다.
  • 다른 뷰의 메서드나 속성에서 위에서 정의한 뷰 객체의 메서드 및 프로퍼티 호출이 가능하게 한다.

Example 10.5: Using the View Object

from django.contrib.auth.mixins import LoginRequiredMixin 
from django.utils.functional import cached_property
from django.views.generic import UpdateView, TemplateView

from .models import Flavor
from .tasks import update_user_who_favorited

class FavoriteMixin:

    @cached_property
    def likes_and_favorites(self):
        """Returns a dictionary of likes and favorites"""
        likes = self.object.likes() favorites = self.object.favorites() 
        return {
            "likes": likes,
            "favorites": favorites,
            "favorites_count": favorites.count(),
        }

class FlavorUpdateView(LoginRequiredMixin, FavoriteMixin, UpdateView):
    model = Flavor
    fields = ['title', 'slug', 'scoops_remaining']
       
    def form_valid(self, form): 
        update_user_who_favorited(
            instance=self.object,
            favorites=self.likes_and_favorites['favorites'] # 다른 뷰의 메서드를 호출하고 있다.
        )
        return super().form_valid(form)
        
class FlavorDetailView(LoginRequiredMixin, FavoriteMixin, TemplateView):
       model = Flavor

Example 10.6: Using View Methods in flavors/base.html

flavors/ 앱 탬플릿에서도 해당 프로퍼티(likes_and_favorites)를 호출할 수 있다.

{# flavors/base.html #}
{% extends "base.html" %}
{% block likes_and_favorites %} 
<ul>
  <li>Likes: {{ view.likes_and_favorites.likes }}</li>
  <li>Favorites: {{ view.likes_and_favorites.favorites_count }}</li>
</ul>
{% endblock likes_and_favorites %}

10.5 GCBV와 폼 사용하기

이번 절에서 아이스크림 종류를 기록하는 예제를 통해 폼과 뷰의 사용법을 알아본다.

Example 10.7: Flavor Model

아이스크림 종류 모델을 정의하였다.

# flavors/models.py
from django.db import models
from django.urls import reverse

class Flavor(models.Model):
    class Scoops(models.IntegerChoices):
        SCOOPS_0 = 0
        SCOOPS_1 = 1

    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    scoops_remaining = models.IntegerField(choices=Scoops.choices,
                                           default=Scoops.SCOOPS_0)

    def get_absolute_url(self):
        return reverse("flavors:detail", kwargs={"slug": self.slug})

10.5.1 뷰 + 모델폼 예제

가장 단순하고 일반적인 장고 폼 시나리오. 모델을 생성 후 모델에 대한 새로운 레코드를 추가하거나 기존 레코드를 수정한다.

다음 뷰가 있다.
1. FlavorCreateView : 새로운 종류의 아이스크림을 추가하는 폼에 해당한다.
2. FlavorUpdateView : 기존 아이스크림을 수정하는 폼에 해당한다.
3. FlavorDetailView : 아이스크림 추가와 변경 모두에 대한 확인 페이지에 해당한다.

Example 10.8: Building Views Quickly with CBVs

  • 적은 양의 코드로 간단하게 뷰를 구현하였다.
  • 작명 관례를 최대한 따르는게 좋다. FlavorCreateView는 장고 CreateView의 서브 클래스이며 FlavorUpdateView는 장고의 UpdateView의 서브클래스이다.
  • 다만 아래에서 FlavorDetailView가 생성/수정에 대한 확인 페이지는 아니다. (다음 예제에서 구현해본다.)
# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView

from .models import Flavor


class FlavorCreateView(LoginRequiredMixin, CreateView): 
    model = Flavor
    fields = ['title', 'slug', 'scoops_remaining']


class FlavorUpdateView(LoginRequiredMixin, UpdateView): 
    model = Flavor
    fields = ['title', 'slug', 'scoops_remaining']


class FlavorDetailView(DetailView):
    model = Flavor

Example 10.9: Success Message Example

  • django.contrib.messages를 이용해서 레코드가 추가되거나 변경되었다는 것을 FlavorDetailView에 알린다.
  • FlavorCreateView.form_valid()와 FlavorUpdate.form_valid()를 오버라이드 하기 위해 FlavorActionMixin을 정의하였다. (믹스인 패턴 활용)
  • FlavorActionMixin은 이미 존재하는 믹스인이나 뷰를 상속하지 않는다. 믹스인은 가능한 상속체인이 단순해야 좋다.
# flavors/views.py
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView

from .models import Flavor


class FlavorActionMixin:
    fields = ['title', 'slug', 'scoops_remaining']

    @property
    def success_msg(self):
        return NotImplemented

    def form_valid(self, form):
        messages.info(self.request, self.success_msg)
        return super().form_valid(form)


class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin, CreateView):
    model = Flavor
    success_msg = "Flavor created!"


class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin, UpdateView):
    model = Flavor
    success_msg = "Flavor updated!"


class FlavorDetailView(DetailView): 
    model = Flavor

Example 10.10: flavor_detail.html

{% if messages %}
<ul class="messages">
{% for message in messages %}
<li id="message_{{ forloop.counter }}"
{% if message.tags %} class="{{ message.tags }}" {% endif %}>
               {{ message }}
           </li>
{% endfor %} </ul>
{% endif %}

10.5.2 뷰 + 폼 예제

  • ModelForm이 아닌 장고 Form을 이용하고 싶을 때가 있다. 검색폼을 통해 예제를 구현해본다.
  • FlavorListView 하나로 검색과 검색 결과 모두를 처리하겠다.
  • 일반적으로 검색 페이지를 구현할 때 인터넷에서의 관례는 'q'를 검색 파라미터의 이름으로 사용하며 POST 요청이 아닌 GET을 이용하는 것이다. (우리의 예제는 이 관례에 기반을 둔다.)
  • 검색 쿼리에 맞는 검색 결과를 가져오기 위해 ListView에서 지원하는 기본 queryset을 수정한다.
  • ListView의 get_queryset() 메서드를 오버라이드 하면 된다.
from django.views.generic import ListView

from .models import Flavor

class FlavorListView(ListView):
    model = Flavor

    def get_queryset(self):
        # 부모 get_queryset으로부터 queryset을 petch 
        queryset = super().get_queryset()
        
        # GET 파라미터를 받는다.
        q = self.request.GET.get("q")
        if q:
            # 필터된 queryset을 반환
            return queryset.filter(title__icontains=q)
        # 기본 queryset 반환
        return queryset

Example 10.12: Search Snippet of HTML

  • 일반적으로 폼에서 GET 요청을 하는 것은 드문 일이나 검색폼에서는 데이터를 추가 or 변경하는 절차가 아니기에 GET 요청으로 처리하였다.
{# templates/flavors/_flavor_search.html #}
{% comment %}
Usage: {% include "flavors/_flavor_search.html" %}
{% endcomment %}
<form action="{% url "flavor_list" %}" method="GET">
  <input type="text" name="q" />
  <button type="submit">search</button>
</form>

10.6 django.views.generic.View 이용하기

아래와 같은 장점을 갖는다.
1. if 문으로 처리하는 함수 기반 뷰 대체하기
2. get_context_data()와 form_valid() 메서드 뒤에 숨어있는 클래스 기반 뷰에 직접 접근하기

from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.shortcuts import render, redirect
from django.views.generic import View

from .forms import FlavorForm
from .models import Flavor

class FlavorView(LoginRequiredMixin, View):
    def get(self, request, *args, **kwargs):
        # Flavor 객체의 디스플레이를 처리
        flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
        return render(request,
                      "flavors/flavor_detail.html",
                      {"flavor": flavor}
                      )

    def post(self, request, *args, **kwargs):
        # Flavor 객체의 업데이트를 처리
        flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
        form = FlavorForm(request.POST, instance=flavor)
        if form.is_valid():
            form.save()
        return redirect("flavors:detail", flavor.slug)

Example 10.14: Using the View Class to Create PDFs

  • JSON, PDF 또는 다른 비 HTML 콘텐츠를 서비스하기 위해 이용할때도 유용하다.
  • 더 많은 커스텀 로직을 처리하고 믹스인이 필요한 상황이라도 django.viewsgeneric.View가 주는 명료함이 장점이 되기도 한다.
from django.contrib.auth.mixins import LoginRequiredMixin 
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.generic import View

from .models import Flavor
from .reports import make_flavor_pdf

class FlavorPDFView(LoginRequiredMixin, View):
    
    def get(self, request, *args, **kwargs): 
        # Get the flavor
        flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
        
        # create the response
        response = HttpResponse(content_type='application/pdf')
        
        # generate the PDF stream and attach to the response
        response = make_flavor_pdf(response, flavor) 
        return response

0개의 댓글