나의 첫 Django 앱 만들기 - part 3 - 1

이 강좌는 나의 첫 Django 앱 만들기 - part 2 - 2에 이어지는 강좌입니다. 이번 강좌에서도 계속하여 웹 투표 어플리케이션을 만들 것이며, 퍼블릭 인터페이스인 "뷰 (view)"를 만드는 방법에 대해서 집중적으로 알아보겠습니다. 

장고 철학

뷰란 장고 어플리케이션이 사용하는 웹 페이지의 한 타입이며, 일반적으로 특정 함수를 실행하고 특정 템플릿을 가지고 있습니다. 예를 들어, 블로그 어플리케이션이면 다음과 같은 뷰를 가질 수 있습니다.

  • 블로그 홈페이지 – 몇개의 최신 포스트를 보여주는 페이지.
  • 싱글 포스트 페이지 – 싱글 포스트를 위한 퍼마링크 (permalink) 페이지.
  • 일 년간의 아카이브 페이지 – 1월부터 12월 까지의 링크를 디스플레이하는 아카이브 페이지.
  • 한 달간의 아카이브 페이지 – 한 달간의 포스트를 모두 표시하는 페이지.
  • 일 일간의 아카이브 페이지 – 일 일간의 포스트를 모두 표시하는 페이지.
  • 커맨트 액션 – 각 포스트의 커맨트를 핸들링.

우리의 투표 어플리케이션은 다음의 4가지의 뷰를 가지게 됩니다.

  • Question “인덱스” 페이지 – 최신 몇개의 질문들을 보여주는 페이지.
  • Question “디테일” 페이지 – 투표를 할 수 있는 유저폼과 함께 하나의 질문을 보여주는 페이지.
  • Question “결과” 페이지 – 특정 질문에 대한 결과를 보여주는 페이지.
  • 투표 액션 – 특정 질문에 대해 투표를 핸들링.

장고는 웹 페이지와 다른 컨텐츠를 보여주기 위해서 뷰를 사용합니다. 각 뷰는 파이썬의 함수 (메소드나 클래스 베이스 뷰의 경우도 있음) 로 만들어 집니다. 장고는 URL 요청이 들어오면 도메인 네임을 제외한 나머지 URL을 보고 적절한 뷰를 찾습니다. 

가끔 웹 브라우저에서 “ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B”와 같은 복잡하고 보기 않좋은 URL을 본 적이 있을 겁니다. 장고는 이것보다 더 우아한 URL 패턴을 제공합니다.

URL 패턴이란 "/newsarchive/<year>/<month>/"와 같이 일반적인 URL 형식입니다.

URL을 보고 맞는 뷰를 찾기 위해 ‘URLconfs’라는 것을 사용합니다. URLconf은 레귤러 익스프레션으로 표시된 URL 패턴을 뷰에 맵핑하여 줍니다.

이 강좌는 기본적인 URLconfs 사용법만을 알려 드릴 것이며, 더 자세한 사용 방법은 django.core.urlresolvers에서 확인하여 주십시오.

뷰 추가하기

그럼 이제 polls/views.py 파일에 몇개의 뷰를 더 추가하여 봅시다. 이번에 추가할 뷰는 request 이외에 또 다른 인수를 받는, 전에 만들었던 뷰와는 조금 다른 뷰입니다.

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)

새로 만든 뷰를 url() 함수를 이용하여 polls.urls 모듈에 추가하여 봅시다.

polls/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    # ex: /polls/
    url(r'^$', views.index, name='index'),
    # ex: /polls/5/
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    # ex: /polls/5/results/
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    # ex: /polls/5/vote/
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

브라우저 주소창에 “localhost:8000/polls/34/”를 입력하여 주십시오. detail() 메소드가 호출되어 URL과 함께 전달된 ID를 표시해 줍니다. 이번에는 “localhost:8000/polls/34/results/”와 “localhost:8000/polls/34/vote/”를 입력하여 주십시오. 임시 결과 페이지와 투표 페이지를 볼 수 있습니다.

만약에 누군가가 “/polls/34/” 요청을 한다면, 장고는 가장 먼저 mysite.urls 파이썬 모듈은 로딩합니다. 왜냐하면 settings.py 파일의 ROOT_URLCONF에 그렇게 정의 되어 있기 때문입니다. :p 그 다음, urlpatterns 라는 이름의 변수를 찾아 순서대로 레귤러 익스프레션을 확인 합니다. 그 다음, '^polls/'에 매칭되는 패턴을 찾으면, 매칭된 문자열 ("polls/")를 제외한 나머지  문자열인 "34/"를 URLconf인 ‘polls.urls’에게 보내 다음 프로세스를 진행하게 합니다. ‘polls.urls’은 r'^(?P<question_id>[0-9]+)/$'을 찾고 다음과 같이 detail() 뷰 함수를 호출하게 됩니다. 

detail(request=<HttpRequest object>, question_id='34')

question_id='34' 부분의 '34'는 (?P<question_id>[0-9]+)로 부터 캡처한 숫자입니다. 소괄호로 묶인 패턴이 캡처한 문자는 인자로써 뷰 함수에게 전달 됩니다. ?P<question_id> 부분은 패턴이 캡처할 문자를 담을 인수의 이름을 정의하고, [0-9]+ 은 레귤러 익스프레션의 숫자 매치를 뜻합니다.

URL 패턴이 레귤러 익스프레션을 사용하기 때문에 실제로 아무 제한없이 모든 문자열을 캡처할 수가 있습니다. URL에 .html과 같은 익스텐션을 사용할 필요가 없습니다. 그래도 사용하고 싶다면 다음과 같은 패턴을 사용하면 됩니다.

url(r'^polls/latest\.html$', views.index),

그런데 다른 개발자가 웃을 수 있으니 사용하지 않기를 바랍니다. 😝

제대로 된 뷰 만들기

각 뷰는 다음 두개중 어느 하나의 역할을 하게 됩니다. 요청 페이지에 대한 컨텐츠를 포함한 HttpResponse 오브젝트를 리턴하거나, Http404와 같은 에러 페이지를 출력하는 역할입니다. 그밖에도 개발자가 원하는 기능을 넣을 수도 있습니다.

뷰는 데이터베이스의 레코드를 읽는 경우도 있습니다. 템플릿을 사용할 경우도 있고, 외부 (third-party) 템플릿을 사용하는 경우도 있습니다. 원하는 파이썬 라이브러리를 사용하여 PDF 파일, XML, ZIP 파일 등 원하면 뭐든지 만들 수가 있습니다.

결국 장고는 뷰로 부터 HttpResponse나 에러 페이지, 둘 중 하나만 리턴 받으면 됩니다.

part 2 - 1 에서 배운 데이터베이스 API를 뷰안에서 사용하여 봅시다. 인덱스 뷰를 수정하여 데이터베이스에 저장된 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)

# 나머지 뷰 (detail, results, vote) 들은 그대로 두십시오.


웹 브라우저의 주소창에서 "localhost:8000/polls/"를 입력하여 인덱스 페이지를 열어 봅시다. (현재는 이전 강의에서 만든 "What's up?" 이라는 오브젝트 하나만 보일 것입니다.)

그런데 이 방법에는 문제가 하나 있습니다. 페이지의 디자인을 파이썬 코드인 뷰안에 하드코딩했기 때문입니다. 만약에 페이지의 디자인을 변경하고 싶으면, 이 파이썬 코드를 수정해야하는 불편함이 있습니다. 장고의 템플릿 시스템을 사용하여 디자인과 파이썬 코드를 분리하여 봅시다. 뷰가 사용할 수 있는 템플릿을 만들어 봅시다.  

먼저, polls 디렉터리 안에 templates 라는 이름의 디렉터리를 만듭니다. 장고는 이 디렉터리에서 템플릿을 찾게 됩니다.

프로젝트의 셋팅안의 TEMPLATES 은 장고가 어떻게 템플릿을 로드하고 보여주는지에 대해서 설정해 줍니다. 기본 템플릿 백엔드인 DjangoTemplates 의 APP_DIRS 옵션은 True로 설정되어 있습니다. DjangoTemplates 백엔드는 기본적으로 INSTALLED_APPS 에 등록되어 있는 각 어플리케이션 디렉터리안에서 “templates” 이라는 이름의 서브 더렉터리를 찾습니다.

지금 금방 만든 templates 디렉터리안에 polls, 이라는 이름의 새로운 디렉터리를 만들고 그 안에 index.html이란 이름의 템플릿 파일을 만들어 주십시오. polls/templates/polls/index.html와 같은 파일 경로를 가지게 됩니다. app_directories template loader는 위에서 설명한 것처럼 작동하기 때문에 지금 만든 템플릿은 polls/index.html 라는 경로를 사용하여 간단히 참조할 수가 있습니다.

템플릿 네임스페이싱

사실 템플릿을 서브 디렉터리인 polls/templates/polls가 아닌 polls/templates에 바로 저장해서 polls/index.html 경로가 아닌, index.html 파일 이름만을 사용하여 참조를 할 수도 있습니다. 그런데 이것은 아주 안좋은 방법입니다. 왜냐하면 장고는 템플릿의 이름만을 전달 받으면 모든 앱의 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 %}

그럼, 이제 새로 만든 템플릿을 사용하기 위해 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 변수는 템플릿 변수를 파이썬 오브젝트에 맵핑하여 주는 파이썬 사전입니다.

다시 웹 브라우저의 주소창에 “localhost:8000/polls/”를 입력하여 주십시오. 조금 전과 같이 “What’s up”이라는 오브젝트가 보일 겁니다. 하지만 이번에는 question의 디테일 페이지 보여주는 링크로 보일 겁니다.

숏컷: render()

템플릿을 로딩하고, context를 채우고, 템플릿과 함께 HttpResponse 오브젝트를 리턴하는 것은 아주 많이 쓰여지는 코드입니다. 장고는 이러한 코드를 조금 더 간단히 해주는 숏컷을 제공합니다. index() 뷰를 다음과 같이 수정해 봅시다. 

polls/views.py
from django.http import HttpResponse
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)

render 숏컷을 사용하면 loaderHttpResponse를 임포트할 필요가 없음을 알 수 있습니다. 하지만 HttpResponsedetail, results, vote 뷰에서 사용하고 있으니 남겨 두겠습니다.

render()함수는 첫 번째 인자로 request 오브젝트를, 두 번째 인자로 템플릿 이름을, 세번째 옵션 인자로 파이썬 사전을 전달 받습니다. 이 함수는 전달받은 템플릿을 HttpResponse 오브젝트로써 전달받은 context와 함께 리턴합니다. 

한 번 더 “localhost:8000/polls/” 페이지를 확인해 보십시오. 이전과 같은 페이지가 출력 되는 것을 볼 수 있을 겁니다.


blog comments powered by Disqus