Django view (template) 만들기

정현우·2021년 10월 20일
3

Django Basic to Advanced

목록 보기
8/40
post-thumbnail

튜토리얼 이어서

  • 3장을 보고 4장을 이어가길 바란다. 공식 문서 튜토리얼에 내 입맛과 부족한 정보를 담아서 정리하는 것에 가깝다.

django는 대표적으로 MVC(MVT) 디자인 패턴을 가지고 있다. model과 database는 보앗으니 controller(Routing - path)와 View(template)를 살펴보자.

뷰 (view, tempalte)

  • 뷰는 Django 어플리케이션이 일반적으로 특정 기능과 템플릿을 제공하는 웹페이지의 한 종류입니다. 예를 들어, 블로그 어플리케이션의 경우 다음과 같은 뷰를 가질 수 있습니다.

    • Blog 홈페이지 – 가장 최근의 항목들을 보여줍니다.
    • 항목 《세부》(detail) 페이지 – 하나의 항목에 연결하는 영구적인 링크(permalink)를 제공합니다.
    • 년도별 축적 페이지 – 주어진 연도의 모든 월별 항목들을 표시합니다.
    • 월별 축적 페이지 – 주어진 월의 날짜별 항목들을 표시합니다.
    • 날짜별 축적 페이지 – 주어진 날짜의 모든 항목들을 표시합니다.
    • 댓글 기능 – 특정 항목의 댓글을 다룰 수 있는 기능
    • 우리가 만드는 poll 어플리케이션에서 다음과 같은 네개의 view 를 만들어 보겠습니다.
    • 질문 《색인》 페이지 – 최근의 질문들을 표시합니다.
    • 질문 《세부》 페이지 – 질문 내용과, 투표할 수 있는 서식을 표시합니다.
    • 질문 《결과》 페이지 – 특정 질문에 대한 결과를 표시합니다
    • 투표 기능 – 특정 질문에 대해 특정 선택을 할 수 있는 투표 기능을 제공합니다.
  • 장고에서 web pages and other content 들은 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).

  • url pattern은 /newsarchive/<year>/<month>/ 와 같은 형식을 가질 수 있다. URL로부터 뷰를 얻기 위해, Django는 〈URLconfs’라는 것을 사용합니다. URLconf는 URL 패턴을 뷰에 연결합니다. 정확하게는 url dispatcher의 역할을 알아야한다. 해당 내용은 1장에서도 다루고 있다.

뷰 추가하기

  • 추가한 polls APP에 view를 추가해줄 것이다. /mysite/polls/views.py 파일을 아래와 같이 바꿔주자. detail, results, vote 함수를 추가해줬다. 그리고 그에 걸맞게 urls도 바꿔주자.
# /mysite/polls/views.py
from django.shortcuts import render
from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

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)

# ============================================================ #
# /mysite/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'),
]
  • node - express에서 routes를 비유하면 가장 적절할 듯 싶다. java servlet에서는 서블릿의 @WebServlet("/URL") 어노테이션을 사용한 매핑, web.xml을 통한 매핑 / spring 진영에서는 dispatcher가 인식할 수 있도록 주로 controller 단에서 url 매핑해준다. 이런 다른 방식에 비해 node - express 진영과 같이 정말 관리하기 쉽다! 매핑하기도 쉽다! 고 느껴진다.

  • server를 러닝해보고 매핑한 url 접속해보면 아래와 같이 뜨면 성공

  • 브라우저에 《/polls/34/》 를 입력해 보세요. 이 주소에 접속하면 detail() 함수를 호출하여 URL 에 입력한 ID 를 출력할 것입니다. 《/polls/34/results/》 와 《/polls/34/vote/》 도 실행해 보세요. 투표 결과와 투표 페이지의 뼈대가 되는 페이지가 출력될 것입니다.

  • 사용자가 웹사이트의 페이지를 요청할 때, 예로 《/polls/34/》를 요청했다고 하면, Django는 mysite.urls 파이썬 모듈을 불러오게 됩니다. ROOT_URLCONF 설정에 의해 해당 모듈을 바라보도록 지정되어 있기 때문입니다. mysite.urls에서 urlpatterns라는 변수를 찾고, 순서대로 패턴을 따라갑니다. 'polls/'를 찾은 후엔, 일치하는 텍스트("polls/")를 버리고, 남은 텍스트인 "34/"를 〈polls.urls〉 URLconf로 전달하여 남은 처리를 진행합니다. 거기에 '<int:question_id>/'와 일치하여, 결과적으로 detail() 뷰 함수가 호출됩니다.

  • detail(request=<HttpRequest object>, question_id=34) : 문자열의 question_id 부분은 일치하는 패턴을 식별하는 데 사용할 이름을 정의하며 int 부분은 URL 경로의 이 부분과 일치해야 하는 패턴을 결정하는 변환기입니다. 콜론(:)은 변환기와 패턴 이름을 구분합니다.

뷰에 비즈니스 로직 (service or BO) 추가

template file return

  • 각 뷰는 기본적으로 두 가지 중 하나를 하도록 되어 있습니다. 요청된 페이지의 내용이 담긴 HttpResponse 객체를 반환하거나, 혹은 Http404 같은 예외를 발생하게 해야합니다. 나머지는 당신에게 달렸습니다.

  • 당신이 작성한 뷰는 데이터베이스의 레코드를 읽을 수도 있습니다. 또한 뷰는 Django나 Python에서 서드파티로 제공되는 템플릿 시스템을 사용할 수도 있습니다. 뷰는 PDF를 생성하거나, XML을 출력하거나, 실시간으로 ZIP 파일을 만들 수 있습니다. 뷰는 당신이 원하는 무엇이든, Python의 어떤 라이브러리라도 사용할 수 있습니다. Django에 필요한 것은 HttpResponse 객체 혹은 예외입니다. => restapi 형태라면 HttpsResponse Data 형태가 DOC(html등의)이 아니라 그저 json type이 되는 것일 뿐, return object는 HttpsResponse기반이라는 점 잊지말기.

  • 왜냐면, 그렇게 다루는게 편리하기 때문입니다. 튜토리얼 2장의 예제에서 다룬 Django 자체 데이터베이스 API를 사용해봅시다. 새로운 index() 뷰 하나를 호출했을 때, 시스템에 저장된 최소한 5 개의 투표 질문이 콤마로 분리되어, 발행일에 따라 출력됩니다.

# /mysite/polls/views.py
from django.shortcuts import render
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)

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)
  • 여기 몇가지 문제가 있습니다. 뷰에서 페이지의 디자인이 하드코딩 되어 있다고 합시다. 만약 페이지가 보여지는 방식을 바꾸고 싶다면, 이 Python 코드를 편집해야만 할 겁니다. 그럼, 뷰에서 사용할 수 있는 템플릿을 작성하여, Python 코드로부터 디자인을 분리하도록 Django의 템플릿 시스템을 사용해 봅시다. => 이 소리는 HttpResponse가 그냥 str덩어리로 <html><body>HttpResponse로 html, DOC파일 드립니다.</body></html>로 되어 있을 가능성을 말한다. 물론 이게 불가능하고 무조건 틀렸다는 소리가 아니다. 하지만 우리는 '지성인'이다.

  • 우선, polls 디렉토리에 templates라는 디렉토리를 만듭니다. Django는 여기서 템플릿을 찾게 될 것입니다. 그리고 index.html을 추가해줍시다.

<!-- polls/templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</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>
  • 프로젝트의 TEMPLATES 설정은 Django가 어떻게 템플릿을 불러오고 렌더링 할 것인지 기술합니다. 기본 설정 파일은 APP_DIRS 옵션이 True로 설정된 DjangoTemplates 백엔드를 구성합니다. 관례에 따라, DjangoTemplates은 각 INSTALLED_APPS 디렉토리의 《templates》 하위 디렉토리를 탐색합니다. => mysite project를 생성하고 해당 디렉토리 (즉 polls APP이 속한 mysite 프로젝트)에서 settings에서는 TEMPLATES = [..] 를 볼 수 있다. 해당 값에서 APP_DIRS = True 값인 것을 명시하고 있는 것이다.

  • 그리고 템플릿 파일에 {% %}는 python이 data를 SSR(server-side-rendering)할 수 있도록 하는 python django template 구문이다.

  • 사실 공식문서에서는 polls/templates에 바로 index를 만드는 것이 아닌, 이름 혼동 및 충돌을 방지하기 위해 polls/templates/polls/index.html을 추천하고 있다. 즉 App폴더/templates/App명/템플릿파일 형태를 추천한다. => 그래서 그냥 offical하게 추천하는 방식으로 하자! 그리고 views를 아래와 같이 다시 바꿔준다.

# mysite/polls/views.py
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))

# return HttpResponse(template.render(context, request)) 를 
# 더 짧고 쉽게 쓰기 위해 우린 render함수만 따로 import해서 사용가능하다.
# 아래와 같이 index 함수를 바꿀 수 있다. 
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)


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)
  • 모든 뷰에 적용한다면, 더 이상 loader와 HttpResponse를 임포트하지 않아도 됩니다. (만약 detail, results, vote에서 stub 메소드를 가지고 있다면, HttpResponse를 유지해야 할 것입니다.)

  • render() 함수는 request 객체를 첫번째 인수로 받고, 템플릿 이름을 두번째 인수로 받으며, context 사전형 객체를 세전째 선택적(optional) 인수로 받습니다. 인수로 지정된 context로 표현된 템플릿의 HttpResponse 객체가 반환됩니다.

404 error handleing

  • 우린(백엔드) 모든 경우의 수를 대비해야 한다. 즉 기본적으로 어떨때 404를 (또는 400, 403 등)을 뱉을지 catch하고 적절하게 줘야한다는 것이다. 뷰는 요청된 질문의 ID 가 없을 경우 Http404 예외를 발생시켜봅시다!
# mysite/polls/views.py
from django.http import HttpResponse, Http404
... # 중략

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 예외를 발생시킵니다. 조금 후에 polls/detail.html 템플릿에 무엇을 넣을 수 있는지 논의하겠지만, 일단 위의 예제를 동작시키기 위해 아래의 내용이 들어있는 파일을 작성하세요.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {{ question }}
</body>
</html>
  • 만약 객체가 존재하지 않을 때 get() 을 사용하여 Http404 예외를 발생시키는것은 자주 쓰이는 용법입니다. Django에서 이 기능에 대한 단축 기능을 제공합니다. detail() 뷰를 단축 기능으로 작성하면 아래와 같습니다.
# mysite/polls/views.py
from django.shortcuts import get_object_or_404, render
... # 중략
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 예외가 발생합니다.

  • 상위 계층에서 ObjectDoesNotExist 예외를 자동으로 잡아 내는 대신 get_object_or_404() 도움 함수(helper functoin)를 사용하거나, ObjectDoesNotExist 예외를 사용하는 대신 Http404 를 사용하는 이유는 무엇일까요?

  • 왜냐하면, 모델 계층을 뷰 계층에 연결하는 방법이기 때문입니다. Django의 중요한 설계 목표는, 약결합(loose coupling) 을 관리하는 데에 있습니다. 일부 제어된 결합이 django.shortcuts 모듈에서 도입되었습니다.

  • 또한, get_object_or_404() 함수처럼 동작하는 get_list_or_404() 함수가 있습니다. get() 대신 filter() 를 쓴다는 것이 다릅니다. 리스트가 비어있을 경우, Http404 예외를 발생시킵니다. 차이점 예제 보기


템플릿 시스템 사용하기

  • Django에서 template에 대해 조금 더 자세히 살펴보자. jsp, ejs (or pug) 등을 떠올려보자. 하물며 php를 떠올려도 괜찮을 것 같다.

템플릿 언어

1. 템플릿 변수

  • 템플릿 변수는 {{ 와 }} 으로 둘러 싸여 있는 변수로서 그 변수의 값이 해당 위치에 치환된다. 변수에는 Primitive 데이타를 갖는 변수 혹은 객체의 속성 등을 넣을 수 있다.
<h4>
  Name : {{ name }}
  Type : {{ vip.key }}
</h4>

2. 템플릿 태그

  • 템플릿 태그는 {% 와 %} 으로 둘러 싸여 있는데, 이 태그 안에는 if, for 루프 같은 Flow Control 문장에서부터 웹 컨트롤 처럼 내부 처리 결과를 직접 덤프하는 등등 여러 용도로 쓰일 수 있다. 다양한 태그에 대한 자세한 설명은 Built-in Template Tag 를 참조하면 된다. 아래 처음 부분은 if 와 for 태그를 사용한 예이고, 마지막은 CSRF 해킹 공격에 대응하여 토큰을 넣어주는 csrf_token 태그를 사용한 예이다.
{% if count > 0 %}
    Data Count = {{ count }}
{% else %}
    No Data
{% endif %}
 
{% for item in dataList %}
  <li>{{ item.name }}</li>
{% endfor %}
 
{% csrf_token %}

3. 템플릿 필터

  • 템플릿 필터는 변수의 값을 특정한 포맷으로 변형하는 기능을 한다. 예를 들어, 날짜를 특정 날짜 포맷으로 변경하거나 문자열을 대소문자로 변경하는 일등을 할 수 있다.

  • 날짜 포맷 지정
    {{ createDate|date:"Y-m-d" }}

  • 소문자로 변경
    {{ lastName|lower }}

4. 코멘트

  • 템플릿에서 코멘트를 넣는 방법은 크게 2가지이다. 한 라인에 코멘트를 적용할 때는 코멘트를 {# 과 #} 으로 둘러싸면 된다. 또한, 복수 라인 문장을 코멘트할 경우는 문장들을 {% comment %} 태그와 {% endcomment %}로 둘러싸면 된다.
{# 1 라인 코멘트 #}
 
{% comment %}  
  <div>
      <p>
          불필요한 블럭
      </p>
      <span></span>
  </div>
{% endcomment %}

5. HTML Escape

  • HTML 내용 중에 <, >, ', ", & 등과 같은 문자들이 있을 경우 이를 그 문자에 상응하는 HTML Entity로 변환해 주어야 하는데, Django 템플릿에서 이러한 작업을 자동으로 처리해 주기 위해 {% autoescape on %} 템플릿 태그나 escape 라는 필터를 사용한다.

  • 예를 들어, 아래 예제에서 content 라는 변수에 인용부호가 들어 있다고 했을 때, 아래와 같이 autoescape 태그나 escape 필터를 사용해서 자동으로 변환하게 할 수 있다. 만약 이러한 변환을 하지 않으면 HTML이 중간에 깨지게 된다.

{% autoescape on %}     # autoescape 태그
    {{ content }}
{% endautoescape %}
 
{{ content|escape }}    # escape 필터
  • 만약 이러한 HTML escape 혹은 HTML 인코딩 기능을 사용하지 않고, <, >, ', ", & 이 들어간 문자열을 HTML에서 사용하고자 한다면, 각 문자를 HTML Entity로 미리 변환해 주어야 한다. 이러한 변환을 보다 편리하게 하는 한 방법으로 온라인 HTML 인코딩 변환 도구를 사용할 수 있다.

다시 튜토리얼로

  • detail.html template파일을 아래와 같이 써주자.
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</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>

  • 템플릿 시스템은 변수의 속성에 접근하기 위해 점-탐색(dot-lookup) 문법을 사용합니다. 예제의 {{ question.question_text }} 구문을 보면, Django는 먼저 question 객체에 대해 사전형으로 탐색합니다. 탐색에 실패하게 되면 속성값으로 탐색합니다. (이 예에서는 속성값에서 탐색이 완료됩니다만) 만약 속성 탐색에도 실패한다면 리스트의 인덱스 탐색을 시도하게 됩니다.

  • {% for %} 반복 구문에서 메소드 호출이 일어납니다. question.choice_set.all은 Python에서 question.choice_set.all() 코드로 해석되는데, 이때 반환된 Choice 객체의 반복자는 {% for %}에서 사용하기 적당합니다.

템플릿에서 하드코딩된 URL 제거하기

  • polls/index.html 템플릿에 링크를 적으면, 이 링크는 다음과 같이 부분적으로 하드코딩된다는 것을 기억하세요. => 앵커태그의 하이퍼링크에 static한 문자를 때려박으면, 수정할때 다 바꿔야함

  • <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'),
...

URL의 이름공간 정하기

  • 튜토리얼의 프로젝트는 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파일에서 하드코딩된 url을 다시 app_name을 활용해 다음과 같이 바꿔주면 된다. <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

0개의 댓글