이 강좌는 지난 강좌인 나의 첫 Django 앱 만들기 – part 3 – 2에 이어지는 강좌입니다. 이번 강좌에서는 간단한 유저폼을 만들어 보고, 지금까지 만든 코드를 조금 더 단순화해 보겠습니다.

간단한 유저폼 만들기

지난 강좌에서 만든 디데일(“polls/detail.html”)을 업데이트하여 템플릿에 HTML <form> 태그를 추가하겠습니다.polls/templates/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>

코드를 수정한 뒤, localhost:8000/polls/1를 브라우저의 주소창에 입력하여 주십시오. 다음과 같은 화면이 출력되는 것을 확인하여 주십시오.

django polls detail view
위 코드에 대한 짧은 설명:

  • 위의 코드는 각 질문에 대한 선택을 라디오 버튼으로 표시합니다. 각 라디오 버튼의 value는 질문에 관계된 Choice 오브젝트의 ID가 됩니다. 각 버튼의 name은 "choice"입니다. 즉, 유저가 버튼을 선택하고 폼을 실행하면 choice=# (#은 선택된 choice 오브젝트의 ID) POST 데이터가 전송되는 것입니다. 이것은 HTML 폼의 기본 컨셉입니다.
  • 폼의 action 속성을 {% url 'polls:vote' question.id %}으로 method 속성을"post"로 설정하였습니다. 이 폼은 서버에 저장된 데이터를 변경하는 폼이므로 method="post"(method="get"의 반대)를 사용하는 것은 아주 중요합니다. 폼을 이용하여 서버의 데이터를 저장 또는 변경할 때는 항상 method="post"를 사용하는 것을 잊지 마십시오. 이것은 Django에만 적용되는 팁이 아니라, 어떤 웹 개발을 하던간에 적용되는 내용입니다.
  • forloop.counter는 for 루프가 몇 번째 루프를 돌고 있는지를 나타냅니다.
  • POST 폼을 만들때는 크로스 사이트 요청 위조(Cross Site Request Forgeries)를 걱정해야 합니다. 장고는 이런 크로스 사이트 요청 위조로부터 백엔드 서버를 보호해주는 아주 사용하기 간단한 시스템을 제공하고 있습니다. 간단히 얘기하자면, 내부 URL을 타겟으로 갖고 있는 모든 POST 폼은 {%csrf_token %} 템플릿 태그를 사용하면 됩니다.

그럼 이제부터 실행된 폼의 데이터를 핸들링하고, 그리고 그 데이터로 어떤 것을 할 수 있는 장고 뷰를 만들어 볼까요? 혹시 나의 첫 Django 앱 만들기 – part 3 – 1 에서 polls 어플리케이션의 URL 설정파일에 다음과 같은 행을 추가한 것을 기억하시나요?polls/urls.py

url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),

그리고 임시로 더미 vote() 함수를 정의했었습니다. 이번에는 이 vote()를 리얼버전으로 만들어 볼까요? 😁  다음 코드처럼 polls/views.py 파일을 수정하여 주십시오.polls/views.py

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
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):
        # 에러 메세지와 함께 폼을 다시 디스플레이합니다.
        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 는 파이썬 사전과 비슷한 오브젝트이며 POST를 통해 전달된 데이터는 키 이름으로 엑세스할 수 있습니다. 위의 경우에는 request.POST['choice'] 선택된 choice 오브젝트의 ID를 문자열로 리턴합니다. request.POST 의 값들은 항상 문자열입니다.장고는 GET 데이터를 POST와 같은 형식으로 엑세스할 수 있는 request.GET도 제공합니다. 그러나 이 강좌에서는 데이터가 POST 요청으로만 수정 되도록 request.POST 만을 사용할 겁니다.
  • 만약 POST 데이터에 choice가 없으면 request.POST['choice']는 KeyError를 발생시킬 겁니다. 위의 코드는 KeyError가 발생했는지를 확인한 후, 에러 메세지와 함께 같은 유저폼인 question 폼을 다시 보여줍니다
  • Choice 카운트를 증가시킨 후, 일반적인 HttpResponse가 아닌 HttpResponseRedirect를 리턴합니다. HttpResponseRedirect는 유저가 리더렉팅 되는 URL을 인자로 받습니다. (이 경우에 우리가 어떻게 URL을 만드는지 다음 항목에서 설명하겠습니다.)위 코드의 주석에서 설명한 것과 같이, POST 데이터를 성공적으로 핸드링했을 때는 항상 HttpResponseRedirect를 리턴하여야 합니다. 이것은 장고에만 적용되는 팁이 아니고, 모든 웹 개발에 적용되는 것입니다.
  • 위의 예제는 HttpResponseRedirect 생성자 안에서 reverse() 함수를 사용하였습니다. 이 reverse() 함수는 뷰 안에서 URL을 사용할 때 하드코딩을 피할 수 있게 도와줍니다. 이 함수는 우리가 보여주고 싶은 뷰의 이름과 이 뷰를 가르키는 URL 패턴의 일부인 변수를 전달 받는다. 위의 경우는 나의 첫 Django 앱 만들기 – part 3 – 1에서 설정한 URLconf를 사용하여 reverse()는 밑과 같은 문자열을 리턴하게 됩니다.’/polls/3/results/’ 위의 숫자 3은 question.id입니다. 리더렉트된 위의 URL은 다시 'results' 뷰를 호출하여 최종 페이지를 보여줍니다.

나의 첫 Django 앱 만들기 – part 3 – 1에서도 설명을 드렸듯이 request는 HttpRequest의 오브젝트입니다. HttpRequest에 대한 자세한 정보는 request and response documentation를 참고하십시오.

누군가가 한 질문에 대해 투표를 하고나면, vote() 뷰는 질문의 투표결과를 보여주는 result 페이지로 리더렉팅합니다. 이번에는 이러한 투표결과를 보여주는 뷰를 만들어 보죠.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})

이 뷰는 나의 첫 Django 앱 만들기 – part 3 – 1에서 만든 detail() 뷰와 같습니다. 조금 다른 부분이 있다면 템플릿 이름이 다르다는 것 뿐입니다. 이와 같이 비슷한 2개의 뷰를 만드는 중복 문제는 나중에 해결하도록 하겠습니다.

자 그럼 이제 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>

템플릿이 다 만들어 졌으면 localhost:8000/polls/1/ 주소를 웹 브라우저 주소창에 입력하여 1번 ID를 가진 질문에 투표를 해보도록 하죠. 투표를 할 때마다 투표 수가 업데이트된 결과 페이지가 보여질 겁니다. 어떤 선택도 하지않고 폼을 실행하시면 아래 이미지와 같이 에러 메세지가 발생하는 것을 볼 수 있습니다.

초이스를 하나 선택한 후 “Vote” 버튼을 누릅니다.django polls detail view radio button


“/polls/1/results/” 주소로 리더렉트 되어 투표 결과 페이지가 보여집니다.django polls result page


아무 선택도 하지않고 폼을 실행하면 다음과 같이 에러 메세지가 발생합니다.django polls detail view error message


노트

우리의 vote() 뷰는 작은 문제점을 가지고 있습니다. 이 뷰는 가장 먼저 데이터베이스로 부터 selected_choice 오브젝트를 가져와서 새로운 votes값을 계산한 다음, 다시 데이터베이스에 저장합니다. 만약에 두 유저가 동시에 투표를 한다면 문제가 발생할 수 있습니다. 이 때 데이터베이스에 저장된 votes 값이 42 이라고 가정했을때, 두 유저는 동시에 실행한 뷰는 42 라는 값을 받은 후, 둘 다 새로 계산한 43 라는 숫자를 데이터베이스에 저장하게 됩니다. 우리가 기대했던 값인 44가 아닌거죠.

이것을 레이스 컨디션(race condition)이라고 부릅니다. 만약 이것에 대해서 관심이 있으시면 Avoiding race conditions using F()를 읽으시고 이 문제를 해결하는 방법을 배울 수 있습니다.

다음 강좌인 나의 첫 Django 앱 만들기 – part 4 – 2에서는 지금까지 사용한 함수 뷰 대신에 클래스 형식의 제네릭 뷰를 사용하여 보겠습니다.