[Django][The Polls][Chap 1] DB 정보를 template으로 보여주는 view 작성법

Info Notice:
안녕하세요. HwanSeok입니다. The Polls 프로젝트는 docs.djangoproject.com를 참조하여 진행됩니다. 본 포스팅은 전체적인 개발 흐름과 The Polls 프로젝트를 효과적으로 이해하기 위한 설명이 포함되어 있습니다.

결과

제일 먼저 현재까지 진행된 결과부터 보겠습니다.

template, 404, namespace를 사용한 view

먼저 각각의 url에 대응되는 view를 생성해서 연결합니다.

chapter-1-0

그리고 DB에 저장된 데이터를 읽어와서 Template을 통해서 보여줍니다.

만약 데이터가 없으면 404를 보여줍니다.

chapter-1-4

다수의 경로를 한 번에 관리할 수 있도록 url의 namespace와 name을 template에 적용했습니다.

chapter-1-6

진행사항

뷰 설명

In Django, web pages and other content are delivered by views. Each view is represented by a Python function (or method, in the case of class-based views). Django will choose a view by examining the URL that’s requested (to be precise, the part of the URL after the domain name).

뷰 추가하기

1
2
3
4
5
6
7
8
9
10
# 뷰 추가 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# url 매칭 pools/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'),
]
1
2
// 확인
py manage.py runserver

chapter-1-0

사용자가 /polls/34로 접속하면 먼저 mysite/urls.py 모듈을 불러옵니다. 그리고 urlpatterns에 포함된 path pattern에서 include를 통해 polls/url.py를 호출합니다. 이 때 /polls/를 제외한 34만 polls/url.py에 전달되고, <int:question_id>/에 34가 전달되어서 view의 parameter로 전달됩니다.

위 과정에서 include가 34를 추출하는 과정을 캡쳐하고 해당 내용을 keyword 인수로서 뷰 함수에 전달한다는 표현을 사용합니다.

뷰 함수가 하는 일

각 뷰는 HttpResponse 객체를 반환하거나 Http404 같은 예외를 발생합니다. 각 뷰는 데이터베이스의 레코드를 읽을 수도 있고, 파이썬이나 장도의 서드파디 템플릿 서비스를 사용할 수도 있습니다. 또, PDF/XML/ZIP 파일을 만들 수도 있습니다. 뷰에서 비즈니스 로직이 구현됩니다.

먼저 Question 객체 하나를 더 생성합니다.

1
2
3
4
5
py manage.py shell
from polls.models import Choice, Question
from django.utils import timezone
q = Question(question_text="Am I dev shark?", pub_date=timezone.now())
q.save()

하드코딩으로 view 구성하기

그리고 polls/view.py를 아래와 같이 수정합니다.

1
2
3
4
5
6
7
8
9
10
11
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

pub_date를 기준으로 최대 5개까지 객체를 불러오고 ‘, ‘를 사용해서 객체의 question_text를 붙여줍니다. 그리고 페이지를 reload하면 아래의 결과를 볼 수 있습니다.

chapter-1-1

페이지가 보여지는 방식을 바꾸고 싶을 때 하드코딩된 view를 전부 바꿔야하는 문제가 있습니다. 이를 위해서 template를 사용합니다.

template 설정으로 view 구성하기

polls/templates/polls/index.html에 아래와 같이 작성합니다. 이렇게 하위 디렉토리를 많이 만드는 것은 우선 convention 때문입니다. 각 프로젝트 하위에는 여러 개의 앱이 있고, 각각의 앱은 각자의 template이 있습니다. 그리고 django는 template 문서를 찾을 때 {app name}/{template}를 기준으로 찾습니다. 따라서 아래와 같이 추가할 temmplate를 django의 template loader는 polls/index.html로 인식합니다.

여기에 따르면 django-admin으로 생성된 프로젝트는 APP_DIRS가 true로 생성되어서 app 하위의 template 디렉토리에서 template 문서를 찾습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Polls(/polls/)</title>
  </head>
  <body>
    {% 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 %}
  </body>
</html>

template 문서를 view에서 참조하도록 하는 방법은 아래와 같습니다.
django.template의 loader를 이용해서 APP_DIRS 속성에 따라 polls/index.html를 인식합니다. 인식된 template에는 context를 인자로 전달하는데, template에서 if latest_question_list와 같이 사용하는 인자를 context에 담아서 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
12
from django.shortcuts import render
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))

chapter-1-2

render() 이용해 template 사용 간소화 하기

loader에 template의 문서를 참조하도록 하고, context를 전달하는 과정을 아래와 같이 간소화 할 수 있습니다.

1
2
3
4
5
6
def index2(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)

Rasing a 404 Error

polls/view.py에 아래의 내용을 추가합니다.

1
2
3
4
5
6
def detail_template(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})

그리고 polls/urls.py를 아래와 같이 수정합니다.

1
2
3
4
# polls/urls.py
.. 생략 ...
path('<int:question_id>/', views.detail_template, name='detail'),
.. 생략 ...

polls/template/polls/detail.html을 아래와 같이 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Polls(/polls/)</title>
  </head>
  <body>
  {{ question }}
  </body>
</html>

이렇게하면 detail_template view를 만들고, /polls/{question_id}/ 경로와 binging을 했습니다. 현재 Question 객체는 아래와 같이 id가 1인 row 하나만 존재하는 상태입니다.

chapter-1-3

따라서 아래와 같이 있는 정보를 조회할 때와 없는 정보를 조회할 때 아래의 결과를 보여줍니다.

chapter-1-4

Rasing a 404 Error 간소화하기

django에서 객체가 있으면 객체 정보를 가져오고, 없으면 404를 반환하는 함수를 제공합니다.

1
2
3
4
5
6
7
8
# polls/views.py
from django.shortcuts import get_object_or_404, render

from .models import Question

def detail_or_404(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
1
2
3
4
# polls/urls.py
.. 생략 ...
path('<int:question_id>/', views.detail_or_404, name='detail'),
.. 생략 ...

결과는 간소화하기 전과 같습니다.

Info Notice:
view level에서 객체 존재 여부를 판단하는 이유
loose coupling을 위해서 입니다. 객체가 없는 경우 더 높은 레벨이나 model API에서 404를 raise할 수도 있습니다. 하지만 이렇게되면 model layer와 view layer 사이의 coupling이 생깁니다. 모델 레이어에서는 모델 구조에만 신경쓸 수 있도록 도와주는 역할을 수행합니다.
spring boot MVC에서도 model,controller와 service를 분리합니다. service에서 model을 참조하여 DB Access 한 뒤 객체의 정합성에 따라서 적절한 결과를 controller에게 보내주는 것과 같은 이유입니다.

polls/template/polls/detail.html 개선

아래와 같이 수정합니다. 조금 더 상세한 정보를 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Polls(/polls/)</title>
  </head>
  <body>
    <h1>{{ question.question_text }}</h1>
    <ul>
    {% for choice in question.choice_set.all %}
        <li>{{ choice.choice_text }}</li>
    {% endfor %}
    </ul>
  </body>
</html>

chapter-1-5

question.question_textdot-lookup syntax라고 불립니다. django는 아래와 같은 순서대로 dot-lookup을 평가합니다.

  1. dictionary look up on the object
  2. attribute look up
  3. list-index lookup

지금 상황에서는 두 번째 attribute look up으로 사용됩니다. template를 구성하는 자세한 방법은 여기있습니다. 추후 더 알아보겠습니다.

polls/template/polls/index.html 개선

현재는 아래와 같은 상황입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Polls(/polls/)</title>
  </head>
  <body>
    {% 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 %}
  </body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
# 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_or_404, name='detail'),
    ... 이하 생략
]

polls/urls.py에서 path를 ‘‘으로 설정했을 때는 경로 polls/를 의미합니다. polls app의 기본 경로를 django가 polls/로 파악하기 때문입니다. 반면에 index.html에서는 a tag 경로를 hardcode로 작성했습니다. href="/polls//" 이렇게 되면 polls 앱의 이름을 변경하거나 urls.py를 재사용할 때 또는 polls/specifics/등으로 경로를 바꾸고 싶을 때 일일이 다 바꿔주어야 해서 불편합니다.

이를 해결하기 위해서 path('', views.index, name='index'),에서 설정한 이름 name를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Polls(/polls/)</title>
  </head>
  <body>
    {% if latest_question_list %}
        <ul>
        {% for question in latest_question_list %}
          <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
        {% endfor %}
        </ul>
    {% else %}
        <p>No polls are available.</p>
    {% endif %}
  </body>
</html>

만약 아래와 같이 detail의 경로를 수정하면 한 번에 반영이 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
# polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('/specifics/<int:question_id>/', views.detail_or_404, name='detail'),
    ... 이하 생략
]

url의 name 중복을 피하기 위한 namespace 설정

아래와 같이 app_name을 지정해두면 namespace로 사용할 수 있습니다.

1
2
3
4
5
6
7
8
# polls/urls.py
from django.urls import path

from . import views

app_name = 'ns_polls'
urlpatterns = [
... 이하 생략 ...

그리고 아래와 같이 namespace:name의 꼴로 지정을 합니다. detail이라는 이름을 가진 url이 같은 project에 속한 다른 app에도 존재할 때 구분할 수있도록 해주는 역할을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Polls(/polls/)</title>
  </head>
  <body>
    {% if latest_question_list %}
        <ul>
        {% for question in latest_question_list %}
          <li><a href="{% url 'ns_polls:detail' question.id %}">{{ question.question_text }}</a></li>
        {% endfor %}
        </ul>
    {% else %}
        <p>No polls are available.</p>
    {% endif %}
  </body>
</html>

chapter-1-6

Success Notice: 위와 같은 과정을 거쳐 처음에 보았던 결과 페이지를 생성하였습니다. 수고하셨습니다. :+1:

개발환경

  • window 10
  • python 3.6.8
  • django 3.1.5

Leave a comment