이 포스팅은 Django 사이트의 튜토리얼을 정리한 글이다.
이제 투표 어플리케이션에 공개 인터페이스인 《뷰(view)》를 추가해 보자!
뷰는 Django 어플리케이션이 일반적으로 특정 기능과 템플릿을 제공하는 웹페이지의 한 종류다. 예를 들어, 블로그 어플리케이션의 경우 다음과 같은 뷰를 가질 수 있다.
우리가 만드는 poll 어플리케이션에서 다음과 같은 네개의 view 를 만들어 볼것이다.
Django 에서는 뷰에 의해 웹 페이지와 다른 콘텐츠가 전달된다. 각 뷰는 파이썬 함수(클래스 기반 뷰의 경우 메서드)로 표현된다. Django는 요청된 URL(정확하게는 도메인 이름 뒤의 URL 부분)을 보고 뷰를 선택한다.
/newsarchive/<year>/<month>/
이런 URL 패턴이 URL의 일반적인 형식이다.
URL로부터 뷰를 얻기 위해, Django는 URLconfs
라는 것을 사용한다. URLconf
는 URL 패턴을 뷰에 연결한다.
polls/views.py
에 뷰를 추가해보자. 이 뷰들은 인수를 받기 때문에 조금 모양이 다르다.
# 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)
다음의 path()
호출을 추가해서 새로운 뷰를 polls.urls
모듈로 연결하자.
# 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'),
]
이제 브라우저에 《/polls/34/》 를 입력해 접속해보면 detail()
함수를 호출하여 URL 에 입력한 ID 를 출력할 것이다. 《/polls/34/results/》 와 《/polls/34/vote/》 도 실행해보면 투표 결과와 투표 페이지의 뼈대가 되는 페이지가 출력된다.
"《/polls/34/》를 요청 했을 때 detail()` 함수 호출 과정"
1.
mysite.urls
파이썬 모듈을 불러온다(ROOT_URLCONF
설정에 의해 해당 모듈을 바라보도록 지정되어 있기 때문)
2.mysite.urls
에서urlpatterns
라는 변수를 찾고, 순서대로 패턴을 따라간다.
3. 'polls/'를 찾은 후엔, 일치하는 텍스트("polls/")를 버리고, 남은 텍스트인 "34/"를 〈polls.urls〉 URLconf로 전달하여 남은 처리를 진행한다.
4. 거기에 '<int:question_id>/'와 일치하여, 결과적으로 detail() 뷰 함수가 호출된다.
detail(request=<HttpRequest object>, question_id=34)
question_id=34
부분은 <int:question_id> 에서 왔다. 괄호를 사용하여 URL 의 일부를 《캡처》하고, 해당 내용을 keyword 인수로서 뷰 함수로 전달한다. 문자열의 :question_id> 부분은 일치되는 패턴을 구별하기 위해 정의한 이름이며, <int: 부분은 어느 패턴이 해당 URL 경로에 일치되어야 하는 지를 결정하는 컨버터다.
작성한 뷰는 데이터베이스의 레코드를 읽을 수도 있다. 또한 뷰는 Django나 Python에서 서드파티로 제공되는 템플릿 시스템을 사용할 수도 있다. 뷰는 PDF를 생성하거나, XML을 출력하거나, 실시간으로 ZIP 파일을 만들 수 있다. 뷰는 Python의 어떤 라이브러리라도 사용할 수 있다.
각 뷰는 두 가지 중 하나를 하도록 되어 있다.
Django에 필요한 것은 HttpResponse 객체 혹은 예외다.
왜냐면, 그렇게 다루는게 편리하기 때문이다. 튜토리얼 2장의 예제에서 다룬 Django 자체 데이터베이스 API를 사용해보자. 새로운 index() 뷰 하나를 호출했을 때, 시스템에 저장된 최소한 5 개의 투표 질문이 콤마로 분리되어, 발행일에 따라 출력된다.
# 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)
# Leave the rest of the views (detail, results, vote) unchanged
여기에는 몇가지 문제가 있다. Python 코드와 디자인이 분리되어있지 않아 페이지가 보여지는 방식을 바꿀 때 Python 코드를 편집해야만 한다. 이럴 때는 뷰에서 사용할 수 있는 템플릿을 작성하여 Python 코드로부터 디자인을 분리하도록 Django의 템플릿 시스템을 사용해 보자.
polls
디렉토리에 templates
디렉토리 만들기 Django는 여기서 템플릿을 찾게 될것이다. 프로젝트의 TEMPLATES 설정은 Django가 어떻게 템플릿을 불러오고 렌더링 할건지 기술한다. 기본 설정 파일은 APP_DIRS
옵션이 True로 설정된 DjangoTemplates 백엔드를 구성한다. 관례에 따라, DjangoTemplates은 각 INSTALLED_APPS
디렉토리의 《templates》 하위 디렉토리를 탐색한다.
templates
디렉토리 내에 polls
디렉토리를 작성 작성 후 그 안에 index.html
파일을 작성 즉, 템플릿은 polls/templates/polls/index.html
에 있어야 한다. Django 내에서 이 템플릿을 polls/index.html
로 참조할 수 있게 된다.
# 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 %}
이제 템플릿을 이용하여 polls/views.py
에 index 뷰를 업데이트 해보도록 하자.
# 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))
이 코드는 polls/index.html
템플릿을 불러온 후, context를 전달한다. context는 템플릿에서 쓰이는 변수명과 Python 객체를 연결하는 사전형 값이다.
브라우저에서 《/polls/》 페이지를 불러오면 튜토리얼 2장에서 작성한 《What’s up》 질문이 포함된 리스트가 표시된다. 표시된 질문의 링크는 해당 질문에 대한 세부 페이지를 가리킨다.
템플릿에 context를 채워넣어 표현한 결과를 HttpResponse
객체와 함께 돌려주는 구문은 자주 쓰는 용법이다. 따라서 Django는 이런 표현을 쉽게 표현할 수 있도록 단축 기능(shortcuts)을 제공한다. index()
뷰를 단축 기능으로 작성하면 다음과 같다.
# polls/views.py
from django.shortcuts import render
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)
모든 뷰에 적용한다면 더 이상 loader
와 HttpResponse
를 임포트하지 않아도 된다. (만약 detail, results, vote에서 stub 메소드를 가지고 있다면 HttpResponse
를 유지해야 한다.)
render()
함수는 request 객체를 첫번째 인수로 받고, 템플릿 이름을 두번째 인수로 받으며, context 사전형 객체를 세전째 선택적(optional) 인수로 받는다. 인수로 지정된 context
로 표현된 템플릿의 HttpResponse
객체가 반환된다.
이제 질문의 상세 뷰에 태클을 걸어보자. 상세 뷰는 지정된 설문조사의 질문 내용을 보여준다.
# polls/views.py
from django.http import Http404 #추가
from django.shortcuts import render
from .models import Question
# ...
def detail(request, question_id): #추가
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question': question})
새로운 내용이 추가됐다. 뷰는 요청된 질문의 ID 가 없을 경우 Http404 예외를 발생시킨다. 위의 예제를 동작시키기 위해 아래의 내용이 들어있는 detail.html
파일을 작성하자.
# polls/templates/polls/detail.html
{{ question }}
만약 객체가 존재하지 않을 때 get()
을 사용하여 Http404 예외를 발생시키는것은 자주 쓰이는 용법이다. Django에서 이 기능에 대한 단축 기능을 제공한다. detail()
뷰를 단축 기능으로 작성하면 다음과 같다.
# polls/views.py
from django.shortcuts import get_object_or_404, render #추가
from .models import Question
# ...
def detail(request, question_id): #추가
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
get_object_or_404()
함수는 Django 모델을 첫번째 인자로 받고, 몇개의 키워드 인수를 모델 관리자의 get()
함수에 넘긴다. 만약 객체가 존재하지 않을 경우 Http404 예외가 발생한다.
"Http404 를 사용하는 이유?"
상위 계층에서 ObjectDoesNotExist 예외를 자동으로 잡아 내는 대신get_object_or_404()
도움 함수(helper functoin)를 사용하거나, ObjectDoesNotExist 예외를 사용하는 대신Http404
를 사용하는 이유가 뭘까?그 이유는 모델 계층을 뷰 계층에 연결하는 방법이기 때문이다. Django의 중요한 설계 목표는 약결합(loose coupling)을 관리하는 데에 있다. 일부 제어된 결합이
django.shortcuts
모듈에서 도입됐다.
투표 어플리케이션의 detail()
뷰로 되돌아가 context 변수 question
이 주어졌을때, 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 }}
구문을 보면, Django는 먼저 question
객체에 대해 사전형으로 탐색한다. 탐색에 실패하게 되면 속성값으로 탐색한다. (이 예에서는 속성값에서 탐색이 완료되지만) 만약 속성 탐색에도 실패한다면 리스트의 인덱스 탐색을 시도하게 된다.
{% for %}
반복 구문에서 메소드 호출이 일어난다. question.choice_set.all
은 Python에서 question.choice_set.all()
코드로 해석되는데, 이때 반환된 Choice
객체의 반복자는 {% for %}
에서 사용하기 적당하다.
polls/index.html
템플릿에 링크를 적으면, 이 링크는 다음과 같이 부분적으로 하드코딩된다는 것을 기억하자.
# 위의 polls/index.html 코드 中
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
이런 하드코딩된 접근방식의 문제점은 수 많은 템플릿을 가진 프로젝트들의 URL을 바꾸는 게 어렵다는 점이다. 그러나, polls.urls
모듈의 path()
함수에서 인수의 이름을 정의했으므로, {% url %}
template 태그를 사용하여 url 설정에 정의된 특정한 URL 경로들의 의존성을 제거할 수 있다.
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
이것이 polls.urls
모듈에 서술된 URL 의 정의를 탐색하는 식으로 동작한다. 다음과 같이 〈detail〉 이라는 이름의 URL 이 어떻게 정의되어 있는지 확인할 수 있다.
...
# the 'name' value as called by the {% url %} template tag
path('<int:question_id>/', views.detail, name='detail'),
...
만약 상세 뷰의 URL을 polls/specifics/12/
로 바꾸고 싶다면 템플릿에서 바꾸는 것이 아니라 polls/urls.py
에서 바꿔야 한다.
...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...
튜토리얼의 프로젝트는 polls
라는 앱 하나만 가지고 진행하지만 실제 Django 프로젝트는 앱이 몇개라도 올 수 있다. Django는 이 앱들의 URL을 어떻게 구별해 낼까? 예를 들어, polls
앱은 detail
이라는 뷰를 가지고 있고, 동일한 프로젝트에 블로그를 위한 앱이 있을 수도 있다. Django가 {% url %}
템플릿태그를 사용할 때, 어떤 앱의 뷰에서 URL을 생성할지 알 수 있을까?
정답은 URLconf에 이름공간(namespace)을 추가하는 것입니다. 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
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>