[Two Scoops of Django] 8장. 함수 기반 뷰와 클래스 기반 뷰

guava·2021년 9월 30일
1

Two Scoops of Django

목록 보기
8/12
post-thumbnail

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

장고는 함수 기반 뷰(function-based views, FBV)와 클래스 기반 뷰(class-based views, CBV)를 지원한다.

8.1 함수 기반 뷰와 클래스 기반 뷰는 언제 이용할 것인가?

  • 우리는 대부분의 경우 클래스 기반 뷰를 선호한다.
  • 클래스 기반 뷰로 구현했을 경우 특별히 더 복잡해지는 경우나 커스텀 에러 뷰들에 대해서만 함수 기반 뷰를 이용하고 있다.
  • 어떤 뷰를 골라야 할지 모르겠다면 다음 순서도의 도움을 받아 보자.

어떤 개발자들은 대부분의 뷰를 함수 기반 뷰로 처리한다. 그리고 클래스 기반 뷰는 서브 클래스가 필요한 경우에 대해 제한적으로 이용하기도 한다. 이 또한 문제 될 것이 없다.

8.2 URLConf로부터 뷰 로직을 분리하기

  • 장고의 URL 디자인 철학에 따르면 뷰와 URL의 결합은 최대한의 유연성을 제공하기 위해 느슨하게 구성되어야 한다.
  • URL 라우트는 단순하고 명료함을 우선으로 해야하며 장고는 그 방법을 제공한다. 다음은 우리의 경험에서 나온 방법론이다.
    1. 뷰 모듈은 뷰 로직을 포함해야 한다.
    2. URL 모듈은 URL 로직을 포함해야 한다.

Django CBV 스타일 URLconf 모듈 (나쁜 예제)

from django.urls import path
from django.views.generic import DetailView

from tastings.models import Tasting

urlpatterns = [
    path('<int:pk>',
        DetailView.as_view(
            model=Tasting,
            template_name='tastings/detail.html'),
        name='detail'),
    path('<int:pk>/results/',
        DetailView.as_view(
            model=Tasting,
            template_name='tastings/results.html'),
        name='results'),
]

이 코드는 장고의 디자인 철학에 어긋난다.

  1. 뷰와 url, 모델 사이에 느슨한 결합(loose coupling)대신 단단하게 종속적인 결합(tight coupling)이 되어 있다. 뷰에서 정의된 내용이 재사용이 어렵다.
  2. 클래스 기반 뷰들 사이에서 같거나 비슷한 인자들이 계속 이용된다. DRY철학에 위배된다.
  3. URL들의 무한한 확장성이 파괴되었다. 이 안티패턴을 사용함으로써 클래스 상속이 불가능해졌다.

8.3 URLConf에서 느슨한 결합 유지하기

앞서 이야기한 문제를 피하기 위해 views, url을 별도의 파일로 구성해본다.

# testings/views.py
from django.urls import reverse
from django.views.generic import ListView, DetailView, UpdateView

from .models import Tasting 

class TasteListView(ListView):
    model = Tasting

class TasteDetailView(DetailView):
    model = Tasting

class TasteResultsView(TasteDetailView):
    template_name = 'tastings/results.html'

class TasteUpdateView(UpdateView): 
    model = Tasting

    def get_success_url(self):
        return reverse('tastings:detail', kwargs={'pk': self.object.pk})
# testings/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path(
        route='',
        view=views.TasteListView.as_view(),
        name='list'
    ), 
    path(
        route='<int:pk>/',
        view=views.TasteDetailView.as_view(),
        name='detail'
    ), 
    path(
        route='<int:pk>/results/',
        view=views.TasteResultsView.as_view(),
        name='results'
    ), 
    path(
        route='<int:pk>/update/',
        view=views.TasteUpdateView.as_view(),
        name='update'
    ) 
]

두 개의 파일로 나뉘었고 코드는 늘어났지만 우리가 지향하는 방법이다.

왜 이런 방식이 더 나을까?

  1. 반복되는 작업 하지 않기: 뷰들 사이에서 인자나 속성이 중복 사용되지 않는다
  2. 느슨한 결합: URLConf로부터 모델과 템플릿이름을 제거함으로서 뷰는 뷰이고 URLConf는 URLConf이게 되었다. 하나 이상의 URLConf에서 정의된 뷰의 호출이 가능해졌다.
  3. 한번에 한 가지씩 업무를 명확하고 매끄럽게 처리: 뷰의 로직을 찾기 위해 뷰나 URLConf를 둘 다 뒤지지 않아도 된다.
  4. 클래스 기반의 장점을 살린다: 뷰 모듈에서 표준화된 정의를 가지게 됨으로써 다른 클래스에서도 우리의 뷰를 상속해서 사용할 수 있다.
  5. 무한한 유연성: 뷰 모듈에서 표준화된 정의를 구현함으로 커스텀 로직을 구현 가능하다.

8.4 URL Namespaces 이용하기

namespace를 tastings로 정의하였다. URL이름은 tasting_detail이 아닌 tastings:detail로 작성하였다.

URLConf

# 프로젝트 루트의 urls.py
urlpatterns += [
    path('tastings/', include('tastings.urls', namespace='tastings')), # 8.3절의 tasting/urls.py로 연결됨 
]
# tastings/views.py snippet
class TasteUpdateView(UpdateView): 
    model = Tasting

    def get_success_url(self):
        return reverse('tastings:detail', kwargs={'pk': self.object.pk})

HTML 템플릿 (taste_list.html)

{% extends 'base.html' %}

{% block title %}Tastings{% endblock title %}

{% block content %} 
<ul>
  {% for taste in tastings %} 
    <li>
      <a href="{% url 'tastings:detail' taste.pk %}">{{ taste.title }}</a>
      <small>
        (<a href="{% url 'tastings:update' taste.pk %}">update</a>)
      </small>
    </li>
  {% endfor %} 
</ul>
{% endblock content %}

8.4.1 더 짧고 직관적이며 반복하지 않는 URL 이름 만들기

  • 8.3절에서 path의 name은 tastings_detail이나 tastings_result이 아니다. 앱이나 모델 이름이 빠진 detail이나 results같은 명확한 이름을 갖는다
  • tastings같은 앱 이름을 입력할 필요가 없어졌다.

8.4.2 서드 파티 라이브러리와 상호 운영성 높이기

  • URL이름을 <my_app>_detail 등의 방법으로 부르면 <my_app> 부분이 겹쳐서 문제가 될 수 있다.
  • 이 때 URL 이름 공간을 통해 이를 해결할 수 있다.
  • 다음은 두개의 contact앱이 있을 때, namespace를 통해 해결해보는 코드다.

url.py

# urls.py at root of project
urlpatterns += [
    path('contact/', include('contactmonger.urls', namespace='contactmonger')),
    path('report-problem/', include('contactapp.urls', namespace='contactapp')),
]

contact.html

{% extends "base.html" %}
{% block title %}Contact{% endblock title %} 
{% block content %}
<p>
<a href="{% url 'contactmonger:create' %}">Contact Us</a>
</p> <p>
<a href="{% url 'contactapp:report' %}">Report a Problem</a> </p>
{% endblock content %}
  • namespace를 이용해서 이름을 지정하고 contact.html에서 지정한 이름으로 호출하고 있다.

8.5 뷰에서 비즈니스 로직 분리하기

  • 비즈니스 로직을 뷰에다 구현한다면? PDF를 생성하거나 REST API를 추가하거나 혹은 다른 포맷을 지원해야 하는 경우에 장애로 대두될 수 있다.
  • 때문에 모델 메서드, 매니저 메서드 또는 일반적인 유틸리티 헬퍼 함수들을 이용하는 전략을 선호하게 되었다.
  • 비즈니스 로직이 재사용 가능한 컴포넌트가 되고 이를 뷰에서 호출하면 확장성이 좋아진다.
  • 처음에는 비즈니스 로직이 포함될 수 있다. 하지만 뷰에서 표준적으로 이용되는 구조 이외에 덧붙여진 비즈니스 로직이 보인다면 해당 코드를 이동시키자.

8.6 Django Views Are Functions

장고의 뷰는 함수로도 볼 수 있다. HTTP 요청 객체를 가져와 HTTP 응답 객체로 바꾼다.

# 함수로서의 장고 함수 기반 뷰
HttpResponse = view(HttpRequest)
# 기본 수학식 형태로 풀이 (remember functions from algebra?)
y = f(x)
# ... 그리고 이를 CBV 예로 변경해 보면 다음과 같다
HttpResponse = View.as_view()(HttpRequest)

8.6.1 뷰의 기본 형태들 (FBV, CBV)

기본 형태를 기억하자

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

# 함수 기반 뷰의 기본 형태 (FBV)
def simplest_view(request):
    # 비즈니스 로직이 여기에 위치한다.
    return HttpResponse('FBV')

# 클래스 기반 뷰의 기본 형태 (CBV)
class SimplestView(View):
    def get(self, request, *args, **kwargs):
        # 비즈니스 로직이 여기에 위치한다.
        return HttpResponse('CBV')

기본 형태가 중요한 이유

  1. Sometimes we need one-off views that do tiny things.
  2. 가장 단순한 장고 뷰를 이해했다는 것은 장고 뷰의 역할을 명확히 이해했다는 것이다
  3. 함수 기반 뷰는 HTTP method 중립이지만 Django CBV는 특정 HTTP method의 선언이 필요하다는 것을 알 수 있다. (get, post ..)

8.7 locals()를 Views Context에 이용하지 말자

  • locals()를 호출형으로 반환하는것은 안티패턴이다.
  • 시간을 단축할것같지만 더 긴 시간을 허비하는 결과를 초래한다.
  • 유지보수가 힘들다. 뷰가 어떤것을 반환하려고 하는지 명확하게 파악하기가 힘들다.
# Don't do this!
def ice_cream_store_display(request, store_id):
    store = get_object_or_404(Store, id=store_id)
    date = timezone.now()
    return render(request, 'melted_ice_cream_report.html', locals())
# Don't do this!
def ice_cream_store_display(request, store_id):
    store = get_object_or_404(Store, id=store_id)
    now = timezone.now()
    return render(request, 'melted_ice_cream_report.html', locals())
  • 위 두 예제의 차이점을 찾는데 얼마나 걸렸는가? 단순한 예제에서도 이렇기 때문에 더 복잡한 코드라면 문제가 커진다.
  • 다음과 같이 명시적인 context를 사용하라.
def ice_cream_store_display(request, store_id): 
    return render(
        request,
        'melted_ice_cream_report.html',
        {
            'store': get_object_or_404(Store, id=store_id),
            'now': timezone.now()
        }
    )

8.8 요약

  • 함수 기반 뷰, 클래스 기반 뷰를 언제 이용하는지와 선호하는 패턴을 다루었다.
  • URLConf에서 뷰 로직을 분리하는 기법을 이야기했다.
  • 뷰 코드는 views.py모듈에, URLConf코드는 앱의 urls.py모듈에 소속되어야 한다.
  • 클래스 기반 뷰를 사용하면 객체 상속을 이용함으로써 코드를 재사용하기 쉬워지고 디자인을 유연하게 할 수 있다.

0개의 댓글