IT/WEB

[Django] 튜토리얼 ⑥ : 템플릿과 URL 시스템 그리고 제너릭 뷰

개발자 두더지 2020. 9. 10. 00:05
728x90

이전의 포스팅의 내용과 이어집니다. 

 

템플릿 시스템 사용하기


투표 어플리케이션의 detail()뷰로 되돌아가보자. context변수 question이 주어졌을 때 보여질 상세 화면은 polls/templates/polls/detail.html에서 설정하면 된다. 우선 아래의 코드로 같이 설정하였다.

#polls/templates/polls/detail.html

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

템플릿 시스템은 변수의 속성에 접근하기 위해 점-탐색(dot-lookup) 문법을 사용한다. 위 코드의 {{ question.question_text }}구문을 살펴보면, 장고는 먼저 question객체에 대해 사전형으로 탐색한다. 탐색에 실패하게 되면 속성값으로 탐색한다(이 예제의 경우 속성값에서 탐색이 완료되지만 말이다). 만약 속성 탐색에도 실패하면 리스트의 인덱스 탐색을 시도한다.

{% for %} 반복 구문에서 메소드 호출이 일어난다. question.choice_set.all은 Python에서 question.choice_set.all()코드로 해석되는데, 이때 반환된 Choice 객체는 반복자에 의해 반복되어 처리된다.

 

하드코딩된 url 제거


 polls/index.html 템플릿에서 아래와 같이 url을 하드 코딩했었다.

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

 이러한 하드코딩은 템플릿이 많아질 경우 일일히 변경해야한다는 불편함이 생긴다. 따라서 polls.urls 모듈의 ptah()함수를 이용해 인수의 이름을 정의하면 {% url %} template 태그를 사용하여 이러한 불편함을 제거할 수 있다. 즉 아래와 같이 변경하면 된다.

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

 이것은 polls.urls 모듈에 서술된 url의 정의를 탐색하는 방식으로 동작한다. 이전에 polls.ulrs는 polls/urls.py에서 'detail'이라는 이름의 URL을 정의했었다. 이 부분을 다시 살펴보자.

...
# the 'name' value as called by the {% url %} template tag
path('<int:question_id>/', views.detail, name='detail'),
...

 여기서 만약 상세 뷰의 URL을 polls/sepecifics/12/로 바꾸고 싶다면 템플릿에서 바꾸는 것이 아니라 polls/urls.py에서 바꿔야한다는 것을 잊지말자.

...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...

 

URL의 이름공간 정하기


 이 예제에서는 polls라는 앱 하나만 작성하였지만, 실제 장고 프로젝트에는 몇 개의 앱을 작성할 수 있다. 여러 개의 프젝트가 있는 경우 장고는 이 앱들의 url을 어떻게 구별할까? 예를 들어, polls 앱은 detail이라는 뷰를 가지고 있지만 동일한 프로젝트에 블로그에 사용되는 동일한 이름의 뷰가 존재하고 있을지도 모른다. {% url %} 템플릿태그를 사용할 때, 어떤 앱의 뷰 url을 생성할지 어떻게 알 수 있을까?

 정답은 URLconf에 이름공간(namespace)를 추가하는 것이다. polls/urlspy 파일에 app_name 변수를 추가하여 어플리케이션의 이름공간을 설정할 수 있다.

# polls/urls.py

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

 이름공간을 명명하였다면 polls/index.html파일에 정의했던 기존의 템플릿 코드를 아래의 코드와 같이 이름공간을 명시하도록 변경하자.

# 변경 전
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

# 변경 후
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

 

html에 <form> 요소 포함시키기


polls/detail.html파일을 다음과 같이 변경해보자.

# polls/detail.html

<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<input type="submit" value="Vote">
</form>

위의 코드를 간략하게 설명하자면

■ 각 질문 선택 항목를 라디오 버튼으로 표시한다. 각 라디오 버튼의 value는 연관된 질문 선택 항목의 ID이다. 각 라디오 버튼의 name은 "choice"이므로 누군가가 라디오 버튼 중 하나를 선택해 폼을 제출하면, POST 데이터인 choice=#을 보낼 것이다. 여기서 #은 선택한 항목의 ID이다.

■ 폼의 action을 {%url 'polls:vote' question.id%}로 설정하고 method="post"를 설정한다(metho="get"이 아니라). method="post"를 사용한다는 점이 굉장히 중요하다. 그 이유는 폼의 송신은 서버쪽의 데이터의 갱신으로 연결되기 때문이다. 서버쪽의 데이터를 갱신하는 폼을 작성할 경우는 method="post"을사용하자. 이것은 장고 고유의 특징이 아닌 웹 개발에 전체에 통용되는 것이다.

■ forloop.counter은 for태그의 루프가 몇 번 실행되었는지 표시하는 값이다.

■ (데이터가 변조될 위험이 있는) POST 폼을 작성하고 있으므로, Cross Site Request Forgeries에 대해 신경써야할 필요가 있다. 다행히도, 장고가 이에 대해 대응하기 위해 사용하기 쉬운 구성품을 제공하고 있으므로 그렇게 걱정할 필요는 없다. 짧게 말하자면, 내부 URL 대상의 모든 POST 폼은 {% csrf_tokem %} 템플릿 태그를 써야한다. 

 아무튼 제출된 데이터를 처리하고 그 데이터를 무언가를 수행하는 뷰를 작성해보자. 이전에 polls/urls.py에서 vote페이지에 대한 URLconf를 다음과 같이 만들었었다.

path('<int:question_id>/vote/', views.vote, name='vote'),

 그리고 polls/view.py에 임의로 vote()함수를 만들었었다. 임의가 아닌 실제 동작을 구현해보자. vote()함수 부분을 다음과 같이 수정해보자. 

from django.http import HttpResponse, HttpResponseRedirect
# polls/views.py

from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

 여기에 이전에 다루지 않았던 내용들이 몇 가지 있다.

■ request.POST는 키로 전송된 자료에 접근할 수 있도록 해주는 사전과 같은 객체이다. 여기의 request.POST['choice']는 선택된 설문의 ID를 문자열로 반환한다. request.POST의 값은 항상 문자열이다. 참고로 장고는 GET 자료에 접근하기 위해 동일한 방법인 request.GET을 제공한다. 그러나 POST 요청을 통해서만 자료가 수정되게 하기 위해 명시적으로 코드에 request.POST를 사용하고 있다.

■ 만약 POST 자료에 choice가 없으면, request.POST['choice']는 KeyError가 일어난다. 위의 코드는 KeyError를 체크하고, choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시 보여준다. 

■ 투표 수가 증가한 후에는 코드는 HttpResponse가 아닌 HttpResponseRedirect를 반환하고, HttpResponseRedirect는 하나의 인수를 받는다. 여기에서의 인수는 사용자가 재전송될 URL(리다이렉트될 URL)이다. 위 코드의 코맨드에서 말하고 있듯, POST 데이터가 성공적으로 전송된 경우 HttpResponseRedirect를 돌려줄 필요가 있다. 이것은 장고 고유의 특성이 아닌 웹 개발의 공통적인 특징이다.

■ 이 예에서 HttpResponseRedirect 컨스트랙터 중에 reverse()함수를 사용하고 있다. 이 함수를 사용하면 뷰 함수 속의 하드 코딩 URL을 할 필요가 없어진다. 함수에는 제약하고 싶은 뷰의 이름과 그 뷰에 전달한 URL패턴의 위치 인수를 전달한다. 이 예에서는 이전에 설정한 URLconf를 사용하고 있으므로, revers()함수를 호출하면 다음과 같은 문자열이 반환된다. 

'/polls/3/results/'

 여기서 3은 question.id의 값이다. 리다이렉트가 될 URL은 'results'뷰를 호출해 최종적으로 도달할 페이지를 표시한다. 

 이전의 포스팅에서 설명하였듯, request은 HttpRequest 오브젝트이다. HttpRequest 객체의 상세한 사항은 여기를 참고하길 바란다.

 누군가가 질문에 대해 투표를 하면 vote()의 뷰는 질문의 결과 페이지에 리다이렉트한다. 그러한 뷰는 아래와 같이 작성한다. 이전의 코드에서 results()함수 부분만 변경해주면 된다.

# polls/views.py

from django.shortcuts import get_object_or_404, render


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

detail()함수 부분과 크게 다르지 않다. 템플릿 이름만 다를 뿐이다. 이제 polls/results.html을 만들자. polls/templates/polls/ 아래에 results.html 파일을 새로 만들고 다음과 같이 작성한다.

# polls/templates/polls/results.html

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

 여기까지 끝났다면 웹 브라우저의 /polls/1/ 페이지에서 투표를 할 수 있을 것이고 투표할 때마다 값이 반영된 결과 페이지를 확인할 수 있다. 만약 투표를 하지 않았다면 오류 메시지를 확인할 수 있을 것이다. 

 

제너릭 뷰(generic view,범용 뷰) 사용하기


아까 보았듯 polls/views.py의 detail()과 results() 함수는 굉장히 짧으며 코드 내용이 중복적이다. 투표의 목록을 표시하는 index()뷰도  동일하다. 

 이러한 함수를 가진 뷰는 웹 개발의 일반적인 케이스이다. 즉, URL을 통해 전달된 파라미터에 의해 데이터 베이스로부터 데이터를 가져와 템플렛을 로드하여 렌더링된 템플릿에 넘겨주는 것은 일반적이다. 따라서 매우 자주 사용되는 용법이므로 장고에서는 제너릭 뷰(범용 뷰)라는 쇼트 커트를 제공하고 있다.

 제너릭 뷰란 Python 코드의 사용을 최소화할 수 있도록 자주 사용되는 패턴을 추상화한 것이다. 지금까지 작성한 polls 어플리케이션을 제너릭 뷰 시스템으로 변환하여 코드를 더욱 간단히 해보자. 변환은 그렇게 많은 단계를 거치지 않는다. 그러한 단계는 다음과 같다.

① URLconf를 변환한다.

② 불필요한 뷰를 삭제한다.

③ 새로운 뷰를 장고의 제너릭 뷰로 설정한다.

 이 단계를 함께 작성해보자.

1) URLconf의 수정

먼저 URLconf의 polls/urls.py를 열어 다음과 같이 코드를 변경하자.

# polls/urls.py

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

 2번째와 3번째의 패스 문자열에는 일치하는 패턴의 이름이 <question_id>에서 <pk>로 변환됐다는 것에 주의하자.

2) Views의 수정

 다음은 이전의 index, deatil 과 results의 뷰를 삭제하고 장고의 제너릭 뷰를 사용하자. 그러기 위해 polls/views.py 파일을 열고 다음과 같이 코드를 변경하자. 마지막에 vote()함수는 변경할 필요없다.

# polls/views.py

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    ... # same as above, no changes needed.

 여기에 사용된 ListViewDetailView에 대해서 설명하자면, 각각 '객체의 리스트를 표시' 혹은 '어떤 타입의 객체의 상세 페이지를 표시'한다는 두 가지의 개념을 추상화한 것이다. 

■ 각 제너릭 뷰는 스스로가 어떤 모델에 대해 동작하고 있는지 알아 둘 필요가 있다. 그것은 model 속성을 통해 알려 준다. 

DetailView 제너릭 뷰는 "pk"라는 이름으로 URL로부터 프라이머리 키를 캡쳐하여 전달하므로 제너릭 뷰용으로 question_id를 pk로 변경하였다.

 기본적으로 DetailView범용뷰는 <app name>/<model name>_detail.html이라는 이름의 템플릿을 사용한다. 따라서 템플릿의 이름을 "polls/question_detail.html"이 된다. template_name 속성을 지정하면, 자동 생성된 디폴트의 템플릿 명이 아닌 지정한 템플릿 명을 사용하도록 장고에 전달하는 것도 가능하다. 또한 results리스트 뷰에도 template_name을 지정하면, 결과 뷰와 상세 뷰를 렌더링했을 때, (사실상에서는 어느쪽도 DetailView이지만,) 각각 다르게 보인다.

 역시, ListView 제너릭 뷰도 <app name>/<model name>_list.html템플릿을 기본적으로 사용한다. 하지만 앞서 설명했듯 이미 존재하고 있는 "polls/index.html"템플릿을 그대로 사용하기 위해 ListView에 template_name으로 전달하였다.

 이전까지는 quesiton이나 latest_quetion_list라는 컨텍스트 변수가 포함된 컨텍스를 템플릿에 넘겨주고 있었다. DetailView에서는 question이라는 변수가 자동적으로 전달된다. 그 이유는 장고 모델(Question)을 사용하고 있고 장고는 콘텍스트 변수에 어울리는 이름을 결정해주는 것이 가능하기 때문이다. 한편 ListView에서는 자동적으로 생성된 컨텍스트 변수는 question)list이다. 이것을 덮어쓰기 하려면, context_object_name 속성을 전달해 latest_question_list를 대시해서 사용하도록 지정한다. 이 방법의 대채 방법으로는 템플릿 쪽을 바꿔 새로운 디렉토리의 컨텍스트 변수의 이름과 일치하도록 하는 것이 있다. 그러나 사용하고 싶은 변수명을 장고에 전달하는 쪽이 간단하다.

 이제 서버를 실행해 새로운 제너릭 뷰 베이스의 투표 앱을 사용해보자. 참고로 제너릭 뷰에 대한 조금 더 상세한 내용을 알고 싶다면 여기를 참고하길 바란다.


참고자료

docs.djangoproject.com/ko/3.1/intro/tutorial03/1

728x90