❗️ django tutorial를 보고 실습 내용을 정리한 글입니다.
잘못된 부분이 있다면 알려주세요 🙏
뷰란 장고에서는 어플리케이션이 제공하는 특정 기능과 템플릿을 제공하는 웹페이지를 의미한다. 예를 들면 질문 페이지, 투표 페이지 등이 있다. 각 뷰는 python 함수 또는 클래스 기반이면 메서드로 표현된다. 장고는 요청된 url
의 도메인 이름 뒷 부분을 검사하여 뷰를 선택한다.
생성할 뷰는 다음과 같다.
# polls/views.py
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
urlpatterns
에 값을 추가하여 url
로 뷰를 연결한다.
# polls/urls.py
from django.urls import path
from . import views
urlpatterns = [
# ex: /polls/
path('', views.index, name='index'),
# ex: /polls/5/
path('<int:question_id>/', views.detail, name='detail'),
# ex: /polls/5/results/
path('<int:question_id>/results/', views.results, name='results'),
# ex: /polls/5/vote/
path('<int:question_id>/vote/', views.vote, name='vote'),
]
url
이 polls/
뒤에 int
값이 붙으면 이를 question_id
로 인식하고 detail
함수를 호출한다. 만약, url
이 question_id
뒤에 results
가 있으면 results
함수를 호출한다. vote
또한 마찬가지다.
한 예로 사용자가 polls/5/
를 요청했다고 하면 장고는 mysite.urls
파이썬 모듈을 호출한다.
# mysite/urls.py
urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]
urlpatterns
를 보면 url
끝에 polls
가 있으면 polls.urls
모듈을 호출하도록 저번 글에서 설정을 했었다. 그래서 남은 텍스트인 /5
를 찾기위해 polls.urls
모듈로 이동하면 거기에 있는 '<int:question_id>/'
와 텍스트가 일치하게되어 결과적으로 detail
뷰 함수가 호출된다.
detail(request=<HttpRequest object>, question_id=5)
이 예에서는
detail
함수는 이런 파라미터를 가지게 된다.
장고에서 뷰는 데이터베이스의 레코드 읽기, (장고 또는 파이썬에서 제공하는) 템플릿 시스템 사용, PDF 생성, XML 출력, 실시간으로 ZIP 파일 만들기 등 파이썬의 모든 라이브러리를 사용할 수 있다.
여러가지 기능을 뷰에 추가할 수 있으나, 각 뷰들은 두 가지 기능만큼은 꼭 있어야한다. 옳바른 요청이여서 HttpResponse
객체를 반환하거나, 404 not found
같은 예외를 발생하게 해야한다.
# polls/views.py
from django.http import HttpResponse
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
output = ', '.join([q.question_text for q in latest_question_list])
return HttpResponse(output)
이렇게 index
를 수정하면 index
뷰를 호출했을 때, 데이터베이스에 저장된 최소 5개의 질문이 작성일 순으로 출력된다. 이렇게 파이썬 코드로 하드코딩이 되어있어도 정상적으로 작동하지만, 만약 보여지는 방식만 살짝 바꾸는 경우라도 코드 자체를 수정해야한다. 이러한 문제점 때문에 장고는 뷰에서 사용할 수 있는 템플릿을 따로 만들어 파이썬 코드와 디자인을 분리한다.
템플릿은 앱 디렉토리에 templates
라는 디렉토리에 저장하여야 한다. 그러면 장고가 스스로 여기서 템플릿을 찾는다. 이때 templates
바로 밑에 템플릿을 저장하는 것보다 templates/<앱이름>
으로 디렉터리를 하나 더 만든 후 템플릿을 저장하는 것이 좋다.
이유는 이름
이 첫번째로 일치하는 템플릿을 선택하도록 되어있어, 다른 앱의 템플릿을 선택할 수 있다. 그렇기때문에 새 디렉터리를 만들어 이를 구분한다.
# polls/templates/polls/index.html
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
템플릿 코드는 위와 같다. 우리가 흔히 아는 html
태그 외에도 장고만의 특징인 탬플릿 태그
가 있는 것을 볼 수 있다. 브라우저는 html
만 해석할 수 있기 때문에 탬플릿 태그
를 이용하면 파이썬 코드를 html
로 바꿔준다. 위 html
파일은 튜토리얼이기 때문에 짧게 만들었지만, 실제로는 완전한 html
문서를 사용해야한다.
# polls/views.py
from django.http import HttpResponse
from django.template import loader
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template('polls/index.html')
context = {
'latest_question_list': latest_question_list,
}
return HttpResponse(template.render(context, request))
하드 코딩된 index
뷰를 다시 위처럼 수정하여 index.html
를 연결한다. index.html
템플릿을 불러온 후, context
를 html
에 전달한다. context
는 '템플릿에서 쓰인 변수명' : '파이썬 객체'
형태의 딕셔너리 값이다.
# polls/views.py
from django.http import Http404
from django.http import HttpResponse
from django.template import loader
from .models import Question
# ...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
template = loader.get_template('polls/detail.html')
except Question.DoesNotExist:
raise Http404("Question does not exist")
return HttpResponse(template.render({'question': question}, request))
detail
뷰가 요청된 Question
에 id
가 없을 경우, 즉 detail
뷰에 질문이 없을 경우 404 not found
를 발생시킨다. detail
뷰의 템플릿인 polls/detail.html
을 만들어야 한다.
# polls/templates/polls/detail.html
{{ question }}
# 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
객체에 대해 딕셔너리형태로 탐색을 한다. 실패를 하면 속성 값으로 탐색을 하고 이 예제는 속성값이기 때문에 탐색이 종료되지만 만약 실패한다면 리스트의 인덱스 탐색을 시도한다.
question.choice_set.all
은 question
모델을 외래키로하는 모든 choice
모델의 값을 불러오는 명령어이다. 즉 질문에 대한 선택지 값을 모두 들고온다.
# polls/index.html
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
이 코드는 polls/
부분이 하드코딩 되어있다. 이럴경우 url
하나를 바꾸는데 html
코드와 파이썬 코드 모두 바꿔야하는 어려운 일이 된다. polls.urls
모듈의 path()
함수에서 인수의 이름을 정의했으므로 {% url %}
태그를 사용해서 하드코딩을 없애야한다.
# polls/index.html
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
만약 detail
뷰의 url
를 polls/specifics/12
로 바꾸고 싶다면, 템플릿을 바꾸는 것이 아니라 polls/urls.py
하나만 바꾸면 된다.
# polls/urls.py
...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...
현재 튜토리얼에서는 polls
라는 앱 하나만 있지만, 실제 장고 프로젝트는 여러개의 앱이 있을 수 있다. 장고가 {% url %}
태그를 사용할 때, 어떤 앱의 뷰에서 url
를 생성할지 알수 있게하는 방법이 URLconf
에 네임스페이스를 추가하는 것이다. polls/urls.py
에 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
템플릿을 바꿔준다.
# polls/templates/polls/index.html
# before
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
# after
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
앞서 만들었던 폼은 투표 기능을 사용하는데 선택
할 수 있는 폼이 아니라 단순한 문자를 출력하는 폼이였다. 투표는 선택이 가능해야하므로 질문에 라디오 버튼을 만들어야한다.
# 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>
forloop.counter
는 for
태그가 반복을 한 횟수를 나타낸다. POST
를 사용하면 CSRF
공격에 대한 위험이 있기 때문에 이를 예방하기위해 {% csrf_token %}
태그를 사용한다. CSRF
를 막기위해 로그인을 할 때마다 하나의 토큰을 발행하여 form
태그의 POST
요청에 대해서만 이 토큰값이 유효한지 확인한다.
그럼 위처럼 Choice
에 대한 라디오 버튼이 생기고, 하나를 선택한 뒤 Vote
버튼을 누르면 지난 시간에 만들었던 polls/views.py
에 vote
뷰 함수가 호출된다.
간단히 만들었던 polls/views.py
의 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):
# 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,)))
vote
뷰로 request
요청이 오면 request.POST['choice']
을 이용해 선택된 결과를 들고 온다. 이때 request.POST['choice']
에는 Choice
테이블의 id
값이 들어있어 선택된 결과를 테이블에서 가져올 수 있다.
만약 선택을 하지 않을 경우 투표 폼을 "You didn't select a choice."
문구와 함께 다시 보여준다.
선택을 했을 경우 투표 값을 올려주며 HttpResponseRedirect
을 통해 results
뷰로 주소를 이동하도록 한다. POST
로 값을 받은 경우 항상 HttpResponseRedirect
로 반환한다. reverse()
함수는 results
의 url
를 하드코딩 하지 않도록 /polls/3/results/
로 반환해준다.
사실
vote
뷰에서 사용한selected_choice.votes += 1 selected_choice.save()
는 문제가 있다. 두 명의 사용자가 동시에 값을 바꾸면 하나만 계산된다. 이 문제를race condition
즉, 경쟁 상태라고 하는데 이에 대한 해결책이 있다.
이제 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})
detail
뷰와 거의 동일하다. 다만 여기서는 장고에서 지원하는 shortcuts
모듈을 사용해서 간단히 표현했다.
detail
뷰에 대한 디자인도 해야한다. 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>
이제 투표를 한 결과를 보여주는 뷰를 완성했다.
Vote again?
을 누르면 다시 투표할 수 있다.
사실 detail
, vote
, result
이 세가지 뷰는 거의 동일한 기능을 한다. request
를 받으면 템플릿
에 맞춰 데이터베이스에서 가져온 데이터를 리턴한다. 이런 일반적인
기능들은 대부분의 웹에서도 자주 사용하므로 장고에서는 제네릭 뷰
를 지원하여 조금 더 빠르게 이러한 기능들을 구현하도록 도와준다. 코드도 굉장히 간결해진다.
제네릭 뷰는 GENERIC BASE View
, GENERIC DETAIL View
, GENERIC EDIT View
등 아주 다양한 뷰들을 지원하는데 우선 아래와 같은 뷰를 사용할 것이다. 제네릭 뷰에 대한 설명은 여기를 참고하면 된다.
앞서 만들었던 뷰들을 제네릭 뷰로 바꾸기 위해 가장 먼저 URLconf
를 수정해야 한다.
# 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'),
]
detail
과 result
뷰의 question_id
를 pk
로 바꾸었다. DetailView
는 url
에서 추출한 값이 기본 키 값이라고 생각하기 때문에 pk
로 변경해야한다. as_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'
def vote(request, question_id):
... # same as above, no changes needed.
polls/views.py
를 제네릭뷰로 수정하면 위처럼 코드가 매우 간단해진다. 따로 데이터베이스에서 값을 로드하는 코드가 없이 model
속성을 사용하면 자동으로 Question
테이블을 가지고 온다. 또한 template_name
속성을 사용하면 render
이라는 메소드 없이 템플릿을 렌더링할 수 있다.