본문 바로가기
파이썬/장고(django)

[Django] 튜토리얼4. 폼과 제네릭 뷰(Forms and generic views)

by 책 읽는 개발자_테드 2022. 6. 26.
반응형

이 글은 장고 공식 홈페이지의 튜토리얼을 번역하고, 직접 실습하는 과정을 정리합니다.

세 번째 튜토리얼(https://scshim.tistory.com/595)과 이어지는 네 번째 글입니다.

 

목차

· 최소한의 form 작성하기

· generic views 작성하기: 코드는 짧을 수록 좋다


최소한의 form 작성하기

이전 튜토리얼(https://scshim.tistory.com/595)에서 작성한 polls/detail.html을 수정한다.

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
   <legend><h1>{{ question.question_text }}</h1></legend>
   {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
   {% 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 %}
</fieldset>
<input type="submit" value="Vote">
</form>

 

- 위 템플릿은 각 질문 선택에 대한 radio 버튼을 보여준다. 각 radio 버튼의 값은 선택된 id와 연관되어있다. 각 radio 버튼의 이름은 “choice”이다. 이는 누군가 라디오 버튼을 누르고, form을 제출하면 POST 데이터 choice=# (#은 선택한 choice의 id)가 전송되는 걸 의미한다.

- 첫 번째 줄을 보면 form의 action으로 {% url ‘polls:vote’ question.id %}, method=”post”을 설정했다. method를 post로 설정하면 서버사이드의 데이터를 변경한다.

- forloop.counter는 for 태그가 루프를 통과한 횟수를 나타낸다.

- POST form을 생성했기 때문에 Cross Site Forgeries를 걱정해야한다. 장고는 이것을 대비한 시스템을 제공한다. 따라서 모든 내부 URL을 목표로한 모든 POST form은탬플릿 태그를 사용해야한다.

- Cross Site Forgeries: 사이트 간 요청 위조를 의미하며 웹사이트 취약점 공격의 하나로 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제 등)를 특정 웹사이트에 요청하게 하는 공격을 의미한다.

 

이제 제출된 데이터를 처리하는 장고 view를 생성하자. polls/urls.py에 아래 코드를 추가한다.

 

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

 

그리고 vote() 함수 더미 구현을 만든다. polls/views.py에 아래 코드를 추가한다.

 

from django.http import HttpResponse, HttpResponseRedirect
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):
       # 질문 투표 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()
 
       # POST 데이터를 성공적으로 처리한 후에는 항상 HttpResponseRedirect를 반환한다.
       # 이렇게 하면 사용자가 뒤로 가기 버튼을 눌렀을 때 데이터가 두 번 게시되는 것을 방지할 수 있다.
       return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

 

- request.POST는 dictionary와 유사한 객체로, 제출된 데이터를 key 이름으로 접근할 수 있다. 이 경우, request.POST[‘choice’]는 선택된 choice의 ID를 문자열(string)로 반환한다.

  ▶ request.POST의 값은 언제나 문자열이다.

  ▶ request는 HttpRequest의 객체다.

 

- 제공된 POST 데이터에 choice가 없다면, request.POST[‘choice’]는 KeyError를 반환한다.  

  ▶ 위 코드는 KeyError를 확인하고, choice가 주어지지 않으면 에러 메세지와 함께 question form을 다시 보여준다.

 

- choice count를 증가시킨 후 HttpResponse가 아닌 HttpResponseRedirect를 반환한다. HttpResponseRedirect는 하나의 인수(사용자가 리다이렉트될 URL)를 사용한다. 

- HttpResponseRedirect 생성자 내부 reverse() 함수를 사용하면, view 함수에서 하드코딩된 URL을 사용하지 않아도된다. 여기에는 제어를 전달하려는 view의 이름과 해당 view를 가리키는 URL 패턴의 변수 부분을 지정된다.

  ▶ 앞선 튜토리얼의 URLconf을 사용하면, 해당 reverse() 함수는 다음과 같은  문자열을 호출한다. 여기서 3은 question.id의 값이다. ex) '/polls/3/results/'

 

질문에 투표한 후, vote() view는 질문을 위한 결과 페이지로 리다이렉트한다. 해당 view를 polls/view.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})

 

다음으로 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/로 이동하여 질문에 대한 투표를 한다. 투표할 때마다 업데이트되는 결과 페이지가 표시된다.



아무것도 선택하지 않고 제출하면, 에러메세지를 보게된다.

 

- 문제점: vote() view는 데이터베이스로 부터 selected_choice 객체를 얻은 후 투표에 대한 새로운 값을 계산한다. 그 후 해당 값을 데이터베이스에 저장한다. 만약 웹사이트에서 두 명의 사용자가 정확히 동시에 투표하면, 문제가 발생한다.

ex) 42 라는 값을 동시에 받은 사용자는 동일한 선택을 하여 44라는 값이 저장되어야 하지만, 43이 계산되어 저장된다. 

이를 경쟁 조건(race condition)이라 하며, 문제를 해결하려면 다음 링크를 참조하자.

https://docs.djangoproject.com/en/4.0/ref/models/expressions/#avoiding-race-conditions-using-f

 

 

generic views 작성하기: 코드는 짧을 수록 좋다 

· detail() 및 results() view는 매우 짧고, 중복된다. 투표 목록을 표시하는 index() view도 마찬가지다. 이러한 view는 URL에 전달된 매개변수에 따라 데이터베이스에서 데이터를 가져오고, 템플릿을 로드하고, 렌더링된 템플릿을 반환하는 기본적인 웹 개발의 일반적인 사례를 나타낸다.

· 장고는 이러한 일반적인 사례를 위한 shortcut인 “generic view” 시스템을 제공한다.

· Generic view는 앱을 작성하기 위해 파이썬 코드를 작성할 필요조차 없는 지점까지 일반적인 패턴을 추상화한다.

 

· Generic view 시스템을 사용하도록 설문조사 앱을 변환하면, 자체 코드를 많이 삭제할 수 있다. 변환하려면 다음 단계를 거쳐야 한다.

1. URLconf를 변환한다.

2. 필요하지 않은 view를 삭제한다.

3. 장고 generic view를 기반으로한 새로운 view를 생성한다.

 

URLconf 고치기

polls/urls.py 파일을 열고, URLconf을 수정한다.

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'),
]

 

두 번째, 세 번째 패턴의 경로 문자열에서 일치하는 패턴의 이름이 <question_id>에서 <pk>로 변경되었다.

 

views 고치기 

polls.views.py 파일을 열고 index, detail, results view를 삭제한다. 그리고 장고 generic view를 사용하여 해당 파일을 수정한다.

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'
…

 

· 위 코드에서는 ListView, DetailView 두 개의 generic view가 사용되었다. 두 가지 generic view는 각각 “객체 list 표시” 및 “특정 타입의 객체에 대한 세부 정보 페이지 표시”의 개념을 추상화한다.

- 각 generic view는 어떤 모델에 따라 작동할지 알아야 한다. 이것은 model 속성을 사용하여 제공된다.

 

- DetailView generic view는 URL에서 캡처된 기본 키 값이 “pk”라고 예상하므로 question_id를 pk로 변경한다.

 

· 기본적으로 DetailView generic view는 <앱 이름>/<모델 이름>_detail.html이라는 템플릿을 사용한다. 

- 여기서는 polls/question_detail.html 템플릿을 사용한다.

 

· template_name 속성은 자동으로 생성된 기본 템플릿 이름 대신 특정 템플릿 이름을 사용하도록 장고에 지시하는데 사용된다.

 

· ListView generic view는 <앱 이름>/<모델 이름>_list.html이라는 기본 템플릿을 사용한다. 

- 여기서는 기존 polls/index.html 템플릿을 사용하도록 ListView에 지시하도록 template_name을 사용한다.

 

· 앞선 튜토리얼에서 템플릿에는 question 및 latest_question_list 컨텍스트 변수가 포함된 컨텍스트가 제공되었다. DetailView의 경우 question 변수가 자동으로 제공된다. 장고 모델(Question)을 사용하므로 장고는 컨텍스트 변수의 적절한 이름을 결정할 수 있다.

· ListView의 경우 자동으로 생성된 컨텍스트 변수는 question_list다. 이를 오버라이드하기 위해 latest_question_list을 사용하도록 지정하는 대신, context_object_name 속성을 제공한다.

- 다른 접근 방식으로 새로운 기본 컨텍스트 변수와 일치하도록 템플릿을 변경할 수 있지만, 장고에 원하는 변수를 사용하도록 하는 것이 훨씬 쉽다.

 

출처

https://docs.djangoproject.com/en/4.0/intro/tutorial04/

반응형

댓글