7. 투표 목록에 장고 템플릿 사용과 투표 상세보기를 위한 함수 만들기

등록일 : 2020.12.15. 19:29


이전 시간 돌아보기

이전 장에서는 views.index() 함수에서 DB의 Question 데이터를 최신순으로 정렬하여 다섯 개만 뽑아왔다. 이 다섯 개의 투표 오브젝트를 for문으로 순회하며 각각의 question_text를 접근하고 이를 한곳에 모아 원하는 텍스트로 가공하여 결과를 응답으로 화면에 전달해 주었다. views.index()에서 여러 Question의 목록을 가져오는 함수를 만드는 과정이었다.

이제 투표 목록을 더 견고하게 만들어보자. 이번 장에서는 각각의 투표 제목마다 고유의 링크를 만들 것이다. 다음 장에서는 각각의 Question을 선택했을 때 나타나는 상세 화면을 만들 것이다. 상세 화면은 투표 제목과 선택지 및 투표 버튼을 표시하는 detail() 함수를 만들어 볼 예정이다. 그리고 선택지를 골라 투표 버튼을 눌렀을 때 처리해주는 vote() 함수와 투표 결과를 보여주는 results() 화면을 각각 만들어 볼 것이다. 우선 각 함수의 기본 틀만 다음과 같이 만든다.

detail, results, vote 기본 함수 틀 만들기

polls/views.py의 맨 하단에 다음을 추가하자. 기존에 index 함수를 만들어 두었을 테니 이 부분에 이어서 추가하자.

# 기존 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)

urls.py 에 각 함수를 연결하기

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/urls.py를 위와 같이 수정한다.

url path 부분에 <int:question_id>라는 생소한 부분이 추가되었다. <> 꺾쇠괄호로 묶인 부분은 사용자가 입력한 URL로부터 값을 추출할 수 있게 해준다. polls/1/ 이라고 입력하면 url.py의 path를 통해 해당 문장에 도달할 것이고, <int:question_id>에 의해 값이 추출된다. polls/1/ 이므로 여기서는 정수 1을 추출하고 question_id = 1 을 detail()안의 question_id이 받게 된다. 여기에서 int 형식은 0부터 양의 정수(1, 2, 3, ...)만 취급한다. 위의 처음 만들었던 index() 함수는 index(request)만 있지만, detail(request, question_id)로 path에서 입력한 값을 뽑아내어 넘겨받은 것이다. 이는 사용자 입력에 대응하는 결과가 달라질 때 유용하게 사용된다. 그리고 전달받은 값을 views.detail 함수에서 처리해 사용자에게 결과를 돌려줄 것이다.

다시 views.py 의 detail 함수를 보자.

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

'당신은 question %s번을 보고 계십니다.' 라는 응답을 해줄 모양이다. %s는 뒤에 오는 %다음의 question_id로 치환된다. question_id는 사용자 요청에 따라 값이 달라지기 때문에 이렇게 활용 할 수 있다. 요청에 따라 다른 답변을 보여주게 될 때에 사용한다.

아직 detail view는 완성된 것이 아니다. 뒤에서 완성 시킬 예정이다.

html 파일을 만들고 장고 템플릿으로 적용해 보기

polls/views.py 보기

# ...생략...

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)

# ...생략...

지난 시간 위 함수에서 하나의 통째로 된 투표 질문 목록을 가져올 수 있음을 알았다. 각각의 투표 질문 제목에 링크를 붙여 제목을 클릭하면 각각의 투표로 이동하고 싶을 것이다. 이런 표현들은 간단하게 .html 파일을 통해 만들 수 있다.

웹에서 보는 페이지들은 html을 기본적으로 사용하고 있다. 보통 확장자가 .html 이다. 줄여서 .htm으로 쓰기도 한다. 3장에서 https://policy.naver.com/policy/service.html 사이트를 예를 들었던 것이 기억날지 모르겠다. 이렇게 .html로 끝나는 페이지에 접근했다면 이 화면은 html로 만들어졌다고 볼 수 있다. 물론 이런 파일명을 노출하지 않는 경우도 있다. 웹에서 다른 페이지로 이동할 때 링크 걸려 있는 부분을 많이 볼 것이다. 이 부분은 html의 태그로 이루어져 있다. html은 여러 태그의 모임으로 완성이 된다. 대부분 <시작 태그> </종료 태그> 등으로 묶여있고 태그에 따라 종료 태그가 없는 경우도 있다. html 파일을 만들어 보자.

지난 시간에 5개의 투표 목록을 만들어 두었다. 최신 투표 제목 5개를 목록형으로 다음처럼 보여주도록 html을 만들 예정이다. 만약 하지 않았다면 admin 페이지에서 Question을 5개를 만들도록 하자.

<!-- 앞으로 만들 화면 예시, 한 줄씩 나눠서 보여주고 상세보기 링크 걸기-->
* 최신 투표 5(링크 걸기)
* 최신 투표 4(링크 걸기)
* 최신 투표 3(링크 걸기)
* 최신 투표 2(링크 걸기)
* 선호하는 떡볶이 맵기 강도는?(링크 걸기)

우리가 출력할 투표 목록의 모양은 이런 식이 될 것이고, 각각 클릭하면 투표의 상세 화면으로 이동하도록 연결할 것이다. 그럼 이제 제대로 된 html 파일을 만들어 보고 간단한 태그도 배워보자.

polls/templates/polls/index.html 만들기

<!-- 최신 투표 목록-->
<ul>
   <li> 최신 투표 5 </li>
   <li> 최신 투표 4 </li>
   <li> 최신 투표 3 </li>
   <li> 최신 투표 2 </li>
   <li> 선호하는 떡볶이 맵기 강도는? </li>
</ul>

html은 위와 같은 모양으로 만들어진다. 맨 첫 줄은 주석문을 만드는 방식이다. 파이썬에서 #을 사용하면 간단한 코멘트를 남길 수 있었다. html 파일은 처럼 감싸면 된다. <ul></ul> 태그는 Unordered list의 약자로, 보통 문서 만들 때 한 번씩 해보았던 dot로 시작하는 순서 없는 리스트를 만들 때 쓴다. 리스트의 시작 태그와 종료 태그로 나누어져 있다. 이 안에 각각 li(List item) 태그로 <li></li> 로 넣을 수 있다. 두 개 이상의 아이템을 넣는 경우 다음과 같은 모양이 된다.

템플릿으로 사용하기 위해 우리는 polls 폴더 아래 templates 폴더를 만들겠다. 그 안에 한 번 더 polls 폴더를 만들고 index.html 파일을 만들자. polls/templates/polls/index.html 모양 구조의 폴더와 파일로 구성할 것이다. 파일을 만들어 다음과 같이 수정해보자. 이번에는 .py가 아닌 .html 형식의 파일이다.

수정했다면 이제 view.py 에서 해당 html 파일을 넘겨주도록 해보자. 윗부분부터 index 함수까지만 수정한다.

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))

# ...생략...

수정을 완료했으면 이제 http://127.0.0.1:8000/polls 로 이동해보자.

우리가 만든 목록이 보일 것이다. 여기에 이제 각각 클릭하면 각 투표를 클릭하면 투표 상세보기 화면으로 이동하는 링크를 추가하도록 하겠다. 위에서 a 태그를 기억하는가? Anchor 태그로 웹에서는 다른 링크로 연결할 때 쓰인다. 투표가 여러 개 있다고 가정할 때 각각의 페이지를 이동하기 위해서는 각각의 고유 링크를 제공해야 한다. 웹에서는 링크를 잘 만들고 관리하는 것이 중요하다. 또한 한번 만든 링크를 변경한다면 기존에 링크를 저장했던 사람들은 접속해도 링크를 볼 수 없게 될 수 있음으로 이를 잘 관리하고 처리하는 것도 중요하다.

자 이제 다시 polls/templates/polls/index.html을 수정해보자.

<ul>
     <li><a href="/polls/5/"> 최신 투표 5</a></li>
     <li><a href="/polls/4/"> 최신 투표 4</a></li>
     <li><a href="/polls/3/"> 최신 투표 3</a></li>
     <li><a href="/polls/2/"> 최신 투표 2</a></li>
     <li><a href="/polls/1/"> 선호하는 떡볶이 맵기 강도는? </a></li>
 </ul>

http://127.0.0.1/polls 로 돌아가 브라우저의 새로고침을 눌러준다.

이제 링크가 생겼다. 이렇게 웹상에서 다른 페이지나 웹사이트로 이동하고 싶은 경우 <a>로 시작하는 이 Anchor 태그를 사용한다. <a> 태그는 시작 태그 안에href 속성이 함께 사용되었다. 여기에는 이동할 주소를 입력하면 된다. <a href="https://www.google.com/"> 구글로 이동 </a> 같은 형식으로 만들면 구글로 이동 처럼 링크가 생긴다. 이 부분을 클릭하면 기본으로 현재 보고 있는 창에서 이동한다.

현재 창이 아닌 새 창으로 띄우고 싶다면 target 속성을 추가하여 사용할 수 있다. <a href="https://www.google.com/" target="_blank"> 새 창으로 구글로 이동 </a> 같은 형식으로 만들면 새 창으로 구글로 이동 처럼 링크가 생긴다. 클릭하면 새 창으로 열릴 것이다.

이제 우리가 만든 html의 polls링크를 클릭해 잘 이동되는지 해 보자.

기존에 만들어 뒀던 detail 함수에서 사용자 입력 url을 받아 현재 보고 있는 question 번호를 출력하고 있다. 이 화면은 임시이며 추후 각 투표의 상세보기 화면이 된다.

이처럼 html은 간단하게 만들고 사용하기는 좋지만, 단점도 있다. 우리는 DB를 연동해 사용하다 보니 투표가 분명 하나도 없을 수도 있고, 추후에는 100개 200개로 늘어날 수도 있다. 매번 데이터가 생길 때마다 일일이 index.html 파일을 수정하게 된다면 참 피곤한 일이 될 것이다.

html 태그만으론 우리가 Python에서 배운 for문 같은 문법 요소를 사용할 수 없다. 하지만 이를 가능하게 해주는 기능이 장고에 있다. 바로 장고의 template 기능이다. 이를 사용하여 다양한 프로그래밍 요소들(조건문, 반복문 등)을 적용할 수 있도록 돕는다. 장고 템플릿을 이용하여 이제 페이지를 꾸며줄 html 파일을 만들어보자.

그리고 위의 index.html은 결정적으로 DB 데이터를 사용한 것이 아니라 값들을 수동으로 직접 입력했다. 실제로는 DB값을 가져와 보여주는 것이 맞다.

이 모든 불편사항을 장고의 템플릿 기능을 통해 쉽게 처리할 수 있다. 방금 만들었던 index.html 파일을 이제 장고 템플릿을 통해 바꿔보자.

polls/templates/polls/index.html 수정

<ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
</ul>

기존과 달라진 부분이 보이는가? <ul><li> 태그와 모양은 비슷한데 실제 내용은 없다. 장고 템플릿 문법은 위와 같은 구조로 표현하고 실제로 {% for ... %}{{ }}로 감싼 장고 태그나 값들은 실제로 html 태그처럼 그에 해당하는 데이터로 변환되어 출력된다. 이것이 장고 템플릿에서 제공하는 문법이다. 하나하나 알아보자.

{% for .. in .. %} 
  <!-- 반복할 문장 -->
{% endfor %}

가운데 li를 일단 생략하고 생각해보자. 장고 템플릿 문법으로 {% for ... %} 구조의 태그 문법이 있다. for문은 이전 시간에 학습한 python의 반복문과 유사하다. 차이점은 파이썬은 종료 태그 없이 반복할 문장을 띄어쓰기 4칸으로 구분했지만, .py와 .html은 다른 형식이므로 동일하게 쓰지 못한다. 장고 태그를 사용한 html에서는 시작 태그와 종료 태그가 존재한다.

{% for question in latest_question_list %}
<!-- 반복할 문장 -->
{% endfor %}

조금 더 작성된 for문을 보자. 우리는 views.index 함수에서 최신 투표 질문 리스트를latest_question_list로 정의했었다. 장고 템플릿 태그를 통해 이를 사용할 수 있다.

위에서는 한줄 한줄 작성했지만, 이제는 템플릿의 반복문을 사용해 한 줄로 처리가 가능하다.

{% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
{% endfor %}

실제 이름값을 사용하는 경우는 {{ }}로 묶어줘야 한다. 괄호 여닫는 기호를 두개씩 사용한다.

어떤가? 이제 admin에서 투표 질문 제목을 추가하거나 투표 제목을 바꾸어도, 장고 템플릿을 활용하여 변경 내용이 알아서 적용되기 때문에 할 일이 획기적으로 줄어든다. 이를 위해 그 복잡 하고 힘들었던 모델 세팅과 뷰 함수와 템플릿 세팅을 했던 것이다. 이제 모양이 제법 갖춰졌다.

HttpResponse를 간단히 처리할 수 있는 단축 기능 render 활용하기

위에서는 HttpResponse 응답에 템플릿을 사용하고 이를 돌려주도록 하는 부분이 꽤 복잡했다. 이는 자주 사용되는 기능이지만 표현하는 부분이 다소 길기 때문에 이를 간결히 표현할 수 있는 단축 기능을 제공한다. render를 통해 이 부분이 가능하다.

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)

기존과 비교해보면 템플릿을 불러오던 loader 부분이 사라지고 맨 마지막 return Httpresponse 부분이 render로 통합되었다. render(request, '템플릿 이름', context) 순으로 지정해주면 짧게 처리됨을 알 수 있다.

처음부터 이렇게 간단한 것을 왜 이리 어렵게 설명했을까 하는 독자도 있을 것이다. 처음엔 쉽고 편한 방법이 좋아 보여도 추후에 더 큰 문제나 위기에 도달했을 때 기초 원리를 알고 있다면 정말 많은 도움이 된다. 장고도, 파이썬도, 컴퓨터 과학 분야는 기본기가 매우 중요하다.

장고 템플릿의 if문을 사용해 가능한 투표가 없을 때 처리하기

만약 가능한 투표가 하나도 없다면 어떻게 표현해주면 좋을까? 아마 아무것도 없다면 화면에는 흰 화면만 나올 것이다. 사용자는 사이트가 폐쇄되었다고 생각할 수도 있다. 이는 사용자에게 좋은 경험이 아니다. 서비스 제공자는 사용자에게 나쁜 경험을 심어주면 사용자는 당신의 서비스를 안 좋게 생각하고 떠나 버린다. 좋은 안내 메시지는 사용자에게 좋은 경험이 되고 여러분의 서비스를 신뢰하게 된다.

사용자가 사이트에 접속했을 때 보여주는 문구, 이미지, 디자인, 가능한 다양한 기능 동작(버튼 클릭 등)에 따른 처리 등을 고민하여 만들면 좋은 웹사이트가 된다.

투표 목록 출력 부분을 바꿔보자. latest_question_list가 있으면 보여주고, 가능한 투표가 없다면 'No polls are available.'라는 문구를 출력하고 싶다. 물론 이 메시지도 그다지 사용자 관점에 좋은 것은 아니지만 우선 이대로 해보자. 이런 처리를 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 %}

if로 시작하는 태그가 사용되었다. 이 태그는 그 뒤에 오는 조건이 참(True)일 때만 표시한다. else 부분은 조건절이 거짓(False)일 때만 수행한다. for문은 반복적으로 사용되는 반면, if문은 조건에 따라 다르게 수행할 때 쓰인다. 즉 latest_question_list가 참(True)이 아니라면 else 절을 기준으로 위의 문장까지 수행하지 않고 else 절의 아래 문장을 수행하여 'No polls are available.' 안내 메시지를 표시한다.

if else 문을 프로그래밍에서는 보통 조건문이라고 한다. 조건에 따라 수행 구문(문장)을 달리 할 수 있다.

{% if latest_question_list %}
   <--! 조건절이 참일   동작 -->
{% endif %}

for, endif 와 같이 if 조건절도 마찬가지로 if, endif 로 묶인다는 것을 알 수 있다. 위와 같은 문장인 경우 latest_question_list가 참이면 endif 가 나올 때까지의 문장을 수행 할 것이고, 거짓이면 endif 사이의 문장은 실행되지 않고 Skip 된다.

하지만 거짓인 경우에 다른 동작을 시키고 싶다면 else 문이 추가 할 수 있다.

조건문의 사용 예시를 하나 들어볼까 한다. 여러분은 어떤 웹사이트에 회원가입을 하고자 한다. 회원가입을 위한 이메일 정보를 입력한 후 회원 가입을 버튼을 눌렀을 때 입력한 이메일이 사용 가능한 이메일이면 정상적으로 처리되고 가입시켜야 할 것이고, 사용 불가능한 이메일이나 이미 가입된 메일이라면 가입시키지 말아야 한다. 이렇게 한가지 동작에 처리 결과를 달리해야 하는 경우 if와 같은 조건문으로 처리할 수 있다. 이렇게 프로그램을 만들 때 정말 자주 사용하게 되는 것이 바로 이 조건문이다.

이전에 latest_question_list = 식으로 정의한 문법이 바로 선언문이다. for문과 같이 반복 문장을 사용하게 하는 반목문까지 선언문, 조건문, 반복문만 안다면 기본적인 프로그래밍의 요소를 다룰 수 있게 된다. 여러분은 이를 방금까지 모두 익혔다.

이번엔 else 절이 포함된 문장을 보자.

{% if latest_question_list %}
   <--! 조건절이 참일   동작 -->
{% else %}
   <--! 조건절이 거짓일   동작 -->
    <p>No polls are available.</p>
{% endif %}

크게 이해하는데 어려움은 없을 것이다. 여기서는 투표할 수 있는 목록이 없으면 'No polls are available.'이라는 문장을 출력하기로 했다.

새로운 태그가 보인다. <p></p> 태그로 감쌌는데 이는 html 태그로 paragraph, 문단을 나타내는 태그다. 자주 사용되는 태그이니 꼭 기억해두자.

우리는 이미 Admin에서 다섯 개의 투표 질문을 추가했지만 실제 상황에서는 이를 잘 못 만들어서 추후 삭제하거나, 아직 아무것도 만들지 않은 상태도 있을 수 있다. 또한 프로그램을 만들 때는 항상 데이터 없는 상태를 고려하는 것도 중요하다. 이를 보통 empty states로 부른다. 예를 들어 실제 데이터가 없는 경우지만 화면상에는 뿌려줘야 하는 경우가 있다. 실제 데이터가 없을 수 있고, 검색 기능이 있다면 사용자 검색 결과가 없는 경우도 있을 수 있기 때문에 이런 경우도 고려해야 한다.

다시 처음의 전체 문장으로 수정하고 브라우저에서 확인하자. 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 %}

눈에 보이는 변화는 없을 것이다. 지금은 등록한 투표 질문이 있기 때문에 목록이 잘 보이고 else 절의 내용은 출력되지 않았다. if의 조건에 의해 잘 처리되었다.

not을 사용해 보기

'No polls are available.'을 보고 싶다면 어떻게 해야 할까? Admin에서 Question을 모두 삭제해 보는 방법도 있지만, 실습을 위해 당장 권장하지는 않는다. 대신 약간의 트릭을 이용해 논리적으로 테스트해 볼 방법이 있다. if 다음 not을 붙이는 것이다. 이는 if not 뒤의 결과를 반대로 뒤집는다. not을 붙여 지금 question_list가 있다면 No polls are available을 출력하도록 해보는 것이다. 실제로 논리적으로 맞지 않는 동작이지만 반대되는 동작이 잘 동작한다는 점을 알 수 있기에 의미가 있다고 볼 수 있다. 그리고 조건의 반대되는 조건을 만드는 경우는 프로그래밍에서 매우 유용하게 사용되는 방법이므로 not에 대해서도 기억하자.

index.html 수정

<!-- ...중략... -->
{% if not latest_question_list %}
<!-- ...중략... -->

if not으로 결과가 반대되어 else 절만 수행되었다. 그리고 다음 실습을 위해 not을 제거하여 원래대로 돌려놓자.

URL 개선하기(하드 코딩된 URL 제거하기)

polls/index.html을 조금 개선해보겠다.

polls/index.html 보기

<!-- ...중략... -->
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
<!-- ...중략... -->

각 투표의 상세 링크를 만들어 주는 부분에 /polls/ 를 직접 입력했다. 다른 부분은 장고 템플릿으로 교체되었지만 이 부분은 직접 데이터를 입력했다. 이렇게 프로그래밍에서 직접 데이터를 입력하는 부분을 하드코딩이라고 한다. 이런 것들이 많아지면 추후 앱 이름이 변경되거나 수정이 발생했을 때 일일이 또 찾아 바꿔 줘야 하고, 미처 변경하지 못하게 된다면 링크 오류가 발생해 서비스가 불가능 할 수도 있다. 이 또한 직접 작성하지 않고 템플릿 태그를 사용하여 가져올 수 있다. 이렇게 템플릿을 활용할 수 있는 부분을 최대한 활용한다면 나중에 관리가 더욱 편리해진다. 그럴 일은 많지 않겠지만 url을 변경하고자 하는 경우 템플릿을 사용했다면 템플릿으로 넘겨줄 데이터 하나만 수정하면 된다. 그렇지 않으면 일일이 html의 주소들을 찾아 바꿔주는 고생을 할 수 있다.

참고로 웹으로 만들어지는 고유 주소는 검색 사이트나 다른 사이트에서 참조(링크 걸기)하는 경우도 있고, 종이 매뉴얼 등에 배포된 후 변경하게 되면 기존 주소로 접속할 수 없음으로, 한번 배포된 고유 주소는 바꾸는 것이 좋지 않다. 물론 불가피하게 변경되었다 바뀐 주소로 연결해 줄 수 있지만 이마저도 불가능한 경우도 있다.

polls/index.html 수정

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

/polls/를 직접 입력하는 대신 장고에서 기본으로 제공하는 url템플릿 태그를 활용하여 할 수 있다. urls.py에서 앱 이름을 지정해두고 url 태그를 {% urlapp_name:뷰이름%}으로 사용하면 해당 주소로 변환해주는 기능을 제공한다. 다음과 같이 수정해보자.

urls.py에서 `app_name = 'polls'를 추가한다.

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'),
]

위에서 view.py에 3개의 함수 틀을 만들었고, 이제 그 함수들을 연결하는 경로를 urls.py 에서 연결했다.

그리고 이제 다시 http://127.0.0.1:8000/polls 에서 적용된 링크를 새로고침 후 기존과 링크가 동일하게 잘 연결되어 있는지 확인하자. 눈에 보이는 변화는 없으며 이는 정상이다.

url템플릿 태그로 a 태그의 href="" 안의 값이 이제 자동으로 완성된다. 눈으로 보이는 결과는 위와 차이가 없지만 분명 아래의 방식이 현재는 이해하기 어려울 수 있어도 나중에는 훨씬 편리한 방식이 됨을 알 수 있을 것이다.

각 메뉴를 클릭해 진입해 보았지만, detail 페이지는 실제 question의 상세보기를 완성하지 못했었다. 이제 이 부분도 만들어 보자.

투표 상세보기 화면을 detail() 함수 만들기

views.py 수정

# ...생략... 
from django.shortcuts import get_object_or_404, render # render가 
# ...생략... 

# ...생략... 
def detail(request, question_id): # detail 함수 전체 수정
    question = get_object_or_404(Question, pk=question_id)
    context = {
        'question': question
    }
    return render(request, 'polls/detail.html', context)
# ...생략...

사용자가 투표 목록에서 상세보기 링크를 클릭했을 때 보여주는 상세 화면을 이제 실제 DB에서 가져와 보여주자. 우리는 detail() 함수를 만들어 처리하도록 하자.

index 함수의 경우는 최신 5개의 목록을 가져왔다. DB 데이터가 같다면 어느 사용자가 요청해도 동일한 정보를 가져올 것이다. 하지만 투표 상세보기는 다르다. 어떤 사용자는 최신 투표 5를 누를 것이고, 어떤 사용자는 최신 투표 4를 고를 것이다. 상세보기의 틀(템플릿)은 모두 같고 요청한 투표 번호에 따라 내용만 달리 보여준다면 어려울 것 없다.

초기 detail() 함수 기본 틀을 잡을 때 def detail(request, question_id):에 대해 설명했다. 사용자가 요청한 question_id를 urls.py의 path()문장에서 넘겨받아 사용하기 때문에 그대로 쓰면 된다.

get_object_or_404가 새로 등장했다. 템플릿 적용을 위한 render처럼 이 또한 단축기능으로 제공한다. 이름 그대로 오브젝트를 얻어오거나 또는 404 오류를 발생해 주는 단축 기능이다. 매우 유용한 기능으로 django.shortcuts에서 제공한다. 이 기능을 사용하기 위해 첫 번째 줄에 import가 필요했다.

혹시 인터넷을 하다 보면 웹 페이지에서 404 오류를 본 적이 있는가? https://github.com/아무거나입력 이 이링크를 클릭해보자. 실제로 없는 링크지만, 우리는 웹 브라우저에서는 아무 링크나 입력이 가능하다.

위와 같이 페이지를 찾을 수 없다는 안내 메시지가 나올 것이다. 비슷한 화면이나 404 오류는 한번씩 경험했으리라 생각한다.

이 경우는 굉장히 자주 발생할 수밖에 없다. 왜냐하면 개발자가 제공하지 않은 url에 임의로 우리가 주소를 입력해 접근할 수 있는 특성이 있기 때문이다. 또한 우리가 만들고 있는 투표 앱을 생각해보면 아직 제작되지 않은 투표 id를 접근할 수도 있고, id 형식에 맞지 않게 입력하는 경우도 있을 것이며, 제공되다가 중간에 삭제된 url을 접근할 수도 있다. 또한 개발자가 제공한 링크를 클릭했지만 개발자가 실수로 링크를 잘못 만들었을 수도 있기 때문이다.

다시 get_object_or_404(Question, pk=question_id)를 살펴보자. 여기서는 Question에 대한 오브젝트나 404를 처리해주는 기능을 한다. pk=question_id의 question_id는 사용자가 투표리스트에서 선택하거나 브라우저에 직접 입력한 question_id를 말한다. pk는 Question의 pk이다. PK(Primary key)는 DB에서 다른 항목과 구별할 수 있는 식별자였다. 127.0.0.1:8000/polls/1/을 사용자가 요청했다면 입력받은 question_id 아이디는 1이 된다. 이를 가지고 실제로 DB의 Question 테이블의 Key에 매핑되는지를 체크하여 결과를 돌려준다. 결과가 없다면 404 오류가 날 것이고, 결과가 있다면 해당 오브젝트를 돌려주게 된다.

직접 눈으로 보는 것 보다 좋은 것은 없다. 장고 쉘을 접속하여 직접 값을 확인해보자. 터미널에서 python manage.py shell을 입력하자.

(venv) $ python manage.py shell
Python 3.7.7 (default, Apr 29 2020, 09:50:19) 
[Clang 11.0.0 (clang-1100.0.33.12)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from polls.models import Question
>>> from django.shortcuts import get_object_or_404
>>> question = get_object_or_404(Question, pk=1)
>>> question
<Question: Question object (1)>
>>> question.id
1
>>> question.question_text
'선호하는 떡볶이 맵기 강도는?'
>>>

>>> 부터 동일하게 한 줄씩 입력해 보자. 쉘 진입 후 세 번째 줄 입력 부분에서 장고에서 제공하는 get_object_or_404로 pk=1에 대응되는 question 오브젝트를 가져온다. 여기에서 오브젝트는 장고 파이썬에서 사용 할 수 있도록 불러와진 정보와 값들의 집합체 정도로 생각하면 된다. Admin에서 보이는 정보들이 Question object (1)이런 식으로 표현되었었는데 그 이유를 이제 조금은 알 수 있을 것이다.

자 다시 views.py의 detail 함수의 전체 코드를 보자.

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    context = {
        'question': question
    }
    return render(request, 'polls/detail.html', context)

get_object_or_404를 통해 Question 모델에서 특정 pk의 오브젝트를 가져와서 question이라는 이름을 붙여줬다. 이를 context로 넣어서 결과를 돌려주는 함수를 완성했다. context로 만드는 이유는 여러 가지의 값을 가져오는 경우 한 번에 모아주는 역할을 한다. 이걸 쓰지 않으면 추후에는 render()안의 세 번째 파라미터가 굉장히 길어질 수 있다. 가독성 차원에서 context로 묶어 많이 사용한다. index 함수에서도 question list를 가져오는 부분에서도 쓰였었다.

사용자 입력을 통해 DB를 연동하고, 없는 경우를 처리했으며 있는 경우는 찾은 오브젝트를 question이라는 이름을 붙여 detail.html 로 넘겨줬다.

다음 장에서는 detail.html을 완성하고 다른 함수들도 완성 시킬 것이다.