지난 시간엔 뷰를 추가하고 템플릿을 지정해 원하는 디자인으로 데이터가 출력될 수 있게끔 하였다.
오늘은 질문에 대한 대답을 투표할 수 있는 폼을 만들어 보고 제네릭 뷰의 사용에 대해 알아볼 것이다.
- 투표 폼 만들기
- 뷰에 동작 추가하기
- 제네릭 뷰 사용하기
지난 시간에 만들었던 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>
맨 첫째 줄에 form action=
부분에서는 투표를 완료한 후에 제출 버튼을 누르면 내용이 전달될 url을 지정해준 것이다. 또한 폼을 제출하여 데이터 서버 사이드를 변경하는 동작을 할 때 method="post"
라고 지정해 주는 것을 볼 수 있다. 장고에서 뿐만 아니라 다른 웹 개발에서도 통상적으로 사용되는 개념이니 유념해두자.
코드 중간에 csrf_token
가 보일 텐데 이것은 보안을 위해 추가한 코드이다. 서버와 클라이언트 간 데이터를 제 3자(=해커)가 임의로 변경하지 못하도록 보호할 수 있다.
이렇게 작성한 폼에 사용자가 투표를 하고 제출 버튼을 누르면 데이터는 action
에 명시해둔 url로 데이터가 전달되고 urls.py에서 올바른 패턴을 찾아 연결된 뷰에서 데이터를 처리한다. 이 경우에는 input의 value값이 전달 된다.
투표 폼을 만들었으니 이제 전달된 데이터를 처리할 수 있도록 뷰를 작성해보자.
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()
return HttpResponseRedirect(reverse('polls:results', args=question_id,))
question.choice_set.get(pk=request.POST('choice')
은 question을 외래키로 가지고 있는 모델에서 pk에 해당하는 값을 불러오는 부분이다. 우리가 템플릿에서 post로 값을 보냈기 때문에 post로 보낸 값 중 input의 name이 choice인 값을 가지고 오게 된다. 정상적으로 투표가 되었다면 choice의 votes 값을 1 증가 시키고 저장한다. 데이터 처리가 끝난 후에는 HttpResponseRedirect
를 리턴하게 된다. HttpResponseRedirect
는 POST
와 항상 세트라고 생각하면 된다.
HttpResponseRedirect
는 하나의 인수를 받으며 해당 코드에서는 reverse()함수를 사용하여 url을 구할 수 있도록 했다. 다음과 같은 url이 인자로 전달될 것이다.
polls/3/results/
마지막으로 만약 사용자가 투표를 하지 않고 제출하게 되면 다시 투표 폼을 띄워주며 에러 메세지를 함께 표시한다.
다음은 초이스를 제출한 후 결과 화면을 표시할 수 있도록 해보자. 일단 다음과 같이 results view를 수정한다.
def results(request, question_id):
question = get_object_or_404(Question, pk=quesion_id)
return render(request, 'polls.results.html', {'question': question})
질문을 context로 넘겨주는 간단한 코드를 작성한 뒤 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>
질문에 대한 응답과 응답 수를 표시한다. pluralize
는 복수처리를 하는 부분인데, choice.votes
가 단수일 때에는 vote
로, 복수 일 때에는 votes
로 표시 될 수 있게 하는 것이다.
💡 Race condition과 F( )
vote 뷰에서 votes를 조작하는 코드가 나오는데, 이 부분은 race condition이 발생할 수 있는 부분이라 약간의 수정이 필요하다.
race condition이란 둘 이상의 입력이나 조작 타이밍이 동작에 영향을 줄 수 있는 상태를 일컫는다.
예를 들어 두 명의 사용자가 정확히 같은 시간에 투표를 했다고 했을 때, 파이썬은 입력이 서로의 입력이 일어나기 전의 스레드 상태에서 업데이트를 하기 때문에 둘 중에 하나의 값만 처리하게 된다.
반면 F( )는 파이썬 메모리에서의 값을 기반으로 하는 것이 아니라 save()나 update()를 실행하는 시점의 데이터베이스 값을 기반으로 처리하기 때문에 경쟁상태를 방지 할 수 있다. 따라서 상기의 코드는 다음과 같이 수정할 수 있다.from django.db.models import F selected_choice.votes = F('votes') + 1
지금까지 작성한 뷰를 보면 특정 코드가 반복되는 모습을 볼 수 있다. 이러한 중복을 방지하고 코드 경량화를 위해 클래스 기반으로 뷰를 작성할 수도 있다.
데이터를 받아오고 템플릿을 로드하여 렌더링 된 템플릿을 브라우저에 표시하는 것은 너무나 자주 쓰이는 패턴이기 때문에 장고에서는 이것의 단축 기능을 제공 하는데 그것이 바로 제네릭 뷰이다.
다음 단계를 통해 코드를 경량화 시켜보자.
- URLConf 수정하기
- 불필요한 View 지우고 제네릭 뷰 사용하기
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')
]
detail과 resutls의 url 패턴을 quesion_id에서 pk로 바꾸었다. 뒤에서 볼 제네릭 뷰에서 pk값을 전달받도록 구현이 되어있기 때문이다(통상적으로 그렇기 때문에 미리 구현이 되어있음). 또한 미리 정의된 as_view()함수를 사용하여 뷰로 연결되게 된다.
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
return Question.object.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(....)
상기 코드를 보면 함수 기반으로 작성된 코드보다 훨씬 간략하다는 점을 알 수 있다.
사용할 템플릿 명과 템플릿에서 사용할 모델명을 지정해주기만 하면 된다.
이때 context에 넘겨주는 이름이 모델명과 다르다면 context_object_name 에 내가 쓸 이름을 정해주고 get_queryset 함수를 작성하여 필요한 데이터를 다시 불러오면 된다.
DetailView는 자동생성되는 템플릿 명을 사용하기 때문에 자동생성된 이름 이외의 템플릿명을 쓰려면 따로 명시가 필요하다. 보통 자동 생성되는 템플릿 명은 app name/model name_detail.html
의 형식이기 때문에 이전에 만들어놓은 템플릿을 사용하기 위해서 이름을 지정해 준 것이다.
마찬가지로 ListView도 app name/model name_list.html
로 템플릿 명이 자동 지정되어 우리가 사용할 템플릿 이름을 지정해 주었다.
오늘 배운점은 다음과 같다.
첫번째. POST로 전달된 데이터를 다룰 때는 HttpResponseRedirect를 사용해야한다는 점이다. 그래야 사용자가 뒤로가기 버튼을 눌러도 데이터가 두번 보내지지 않는다.
두번째. 제네릭 뷰의 사용. 제네릭 뷰를 사용하면 미리 정의된 내용이 많아 코드를 훨씬 단축 시킬 수 있다. 하지만 함수형 보다는 직관성이 떨어져서 익숙해지기 전 까지는 함수형으로 연습을 해야 더 이해가 될 것 같다.
다음편도 빠샤!!!🔥
참고:
[유튜브]django 웹 프로그래밍 강좌
Django tutorial part.4