Advanced Templates

RequestContext와 Context Processor

템플릿을 렌더링하기 위해서는 django.template.Context의 인스턴스가 필요하다. 그러나 Django는 django.template.RequestContext라는 서브클래스를 사용하는데, 이는 HttpRequest와 같은 객체나 로그인된 유저의 정보 등을 가지고 있다. render() 함수는 특별히 명시적으로 다른 context 인스턴스를 지정하지 않는 이상 RequestContext를 생성해서 돌려주는 역할을 한다.

Context Processor 사용하기

아래 예제를 보면, app, user, ip_address를 context로 만드는 작업이 view_1view_2에서 중복되고 있다.

from django.template import loader, Context

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am view 1.'
    })
    return t.render(c)

def view_2(request):
    # ...
    t = loader.get_template('template2.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am the second view.'
    })
    return t.render(c)

이와 같이 중복작업을 하게되는 경우, Context Processor를 직접 만들어서 중복작업을 제거하는데 사용할 수 있다.

from django.template import loader, RequestContext
def custom_proc(request):
    # A context processor that provides 'app', 'user' and 'ip_address'.
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
}

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = RequestContext(request,
                       {'message': 'I am view 1.'},
    return t.render(c)

def view_2(request):
    # ...
processors=[custom_proc])
    t = loader.get_template('template2.html')
    c = RequestContext(request,
                       {'message': 'I am the second view.'},
                       processors=[custom_proc])
    return t.render(c)
  • 첫 번째로, 중복되는 Context를 만들어줄 커스텀 함수를 작성한다. 이 함수는 HttpRequest 객체를 인자로 받아서 template context에서 사용할 dictionary를 반환한다.
  • 두 번째로, view 함수에서 Context 대신 RequestContext를 사용한다. RequestContextHttpRequest 객체와 각 view에서 사용할 변수들을 인자로 받고, 선택적으로 list 혹은 tuple로 이루어진 processor들을 인자로 받는다. 위 예제에서는 커스텀 함수로 만들어둔 custom_proc를 입력했다.
  • 이로써 각 view 함수가 app, user, ip_addressContext에 넣어주는 중복작업을 하는 대신, custom_proc를 사용하여 한 곳에서 처리할 수 있도록 변경되었다. 그럼에도 불구하고 각 view 함수들은 필요한 경우 자체적인 변수를 할당하는 등의 처리를 할 수 있기 때문에 유연성을 확보할 수 있게 되었다.

위의 예제에서 사용한 lower-level 방식대신, render라는 Django의 shortcut을 사용하면 아래 예제와 같이 좀 더 간단하게 처리할 수 있다.

from django.shortcuts import render
from django.template import RequestContext

def custom_proc(request):
    # A context processor that provides 'app', 'user' and 'ip_address'.
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
}

def view_1(request):
    # ...
    return render(request, 'template1.html',
                  {'message': 'I am view 1.'},
                  context_instance=RequestContext(
                  request, processors=[custom_proc]
                  )
)

def view_2(request):
    # ...
    return render(request, 'template2.html',
                  {'message': 'I am the second view.'},
                  context_instance=RequestContext(
                  request, processors=[custom_proc]
                  )
)

만약, 프로젝트 전반에 걸쳐서 공통으로 적용해야하는 context가 있다면, Django의 settings.pyTEMPLATES 설정에 context_processors를 추가해둘 수 있다. context_processors는 기본적으로 아래와 같이 적용되어 있다.

'context_processors': [
    'django.template.context_processors.debug',
    'django.template.context_processors.request',
    'django.contrib.auth.context_processors.auth',
    'django.contrib.messages.context_processors.messages',
],

context_processors 설정은 Python 경로 방식으로 지정해야하며, HttpRequest 객체를 인자로 받아서 dictionary를 반환하는 함수라면 어떤 것도 지정할 수 있다. 이렇게 지정된 Context Processor는 list에 등록된 순차적으로 호출되며, 각 processor가 실행될 때마다 각각 반환하는 dictionary가 합쳐져서 최종 결과가 template에 전달된다. 주의할 점은, 서로 다른 processor에서 동일한 이름을 가진 변수를 반환한다면, 먼저 등록된 processor가 반환한 것을 나중에 등록되어 있는 processor가 반환한 것으로 덮어쓰게된다는 점이다. Django가 미리 제공하는 Context Processor는 다음과 같은 것들이 있다. 만약 다음 processor가 활성화되어 있다면 각각의 설명에 해당하는 변수가 모든 RequestContext에 포함된다.

  • auth (django.contrib.auth.context_processors.auth)
    • user: auth.User instance: 로그인을 했다면 로그인된 유저, 그렇지 않다면 AnonymousUser의 instance이다.
    • perms: django.contrib.auth.context_processors.PermWrapper의 instance로써 현재 로그인된 유저의 권한정보를 가지고 있다.
  • debug (django.template.context_processors.debug)
    • DEBUG 옵션이 True로 설정 되어 있고, 요청 IP(request.META['REMOTE_ADDR'])가 INTERNAL_IPS에 해당할 때만 변수가 포함된다.
    • debug: True가 할당되어 있다.
    • sql_queries: {'sql': ..., 'time':, ...} 형태의 dictionary list이다. 요청을 처리하며 발생한 모든 SQL 쿼리가 포함되어 있으며, 쿼리를 처리하는데 걸린 시간정보가 함께 포함된다.
  • i18n (django.template.context_processors.i18n)
    • LANGUAGES: LANGUAGES 설정 값을 그대로 포함한다.
    • LANGUAGE_CODE: 만약 요청에 request.LANGUAGE_CODE가 있다면 이 정보가 포함되며, 그렇지 않다면 설정되어 있는 LANGUAGE_CODE가 포함된다.
  • media (django.template.context_processors.media)
    • MEDIA_URL: MEDIA_URL 설정 정보가 포함된다.
  • static (django.template.context_processors.static)
    • STATIC_URL: STATIC_URL 설정 정보가 포함된다.
  • csrf (django.template.context_processors.csrf)
    • csrf_token 템플릿 태그에서 필요한 토큰을 포함한다. 이것은 Cross Site Request Forgery 방지에 사용된다.
  • request (django.template.context_processors.request)
    • request: 이 processor가 활성화되어 있으면, 모든 요청에서 RequestContextrequest 변수에 포함된다.
  • messages (django.contrib.messages.context_processors.messages)
    • messages: messages 프레임워크를 통해 str 타입의 메시지 list가 포함된다.

Context Processor를 직접 작성하기 위한 가이드라인

  • 각각의 context processor는 가능한 한, 작은 조각으로 이루어진 기능으로 구성하는 것이 좋다. 작게 구성해두면 추후 각 processor들을 모아서 재사용하는데 편리하다.
  • TEMPLATE_CONTEXT_PROCESSORS에 설정된 context processor는 모든 템플릿에서 사용가능하다. 따라서 모든 processor들에서 사용하는 변수들은 이름이 달라야 한다.
  • Django의 공식적인 규칙은은 직접 제작한 context processor들은 context_processors.py라는 이름으로 저장하는 것이다.

자동화된 HTML 회피 처리(auto-escaping)

최종 HTML을 생성하는데 있어서, 위험성을 내포하고 있는 문자들이 변수에 들어가 있을 수 있다. 예를 들어 다음과 같은 템플릿 조각이 있다고 해보자.

Hello, .

누군가가 자신의 이름에 다음과 같은 문자열을 저장했다고 해보자. 그렇다면 name 변수에 다음 문자열이 들어가 있을 수 있다.

<script>alert('hello')</script>

이것은 다음과 같이 렌더링 된다.

Hello, <script>alert('hello')</script>.

이것은 브라우저가 JavaScript의 alert box를 띄운다는 것을 의미한다. 이런 종류의 방식을 활용해서 보안 공격을 시도하는 것을 Cross Site Scripting(XSS) 공격이라고 부른다. Django에서 이런 문제를 회피하기 위해서는 두 가지 방법이 있다.

  • 문제가 있을만한 모든 변수를 escape 필터를 통해 문제가 될만한 HTML 문자열을 변환시키는 것이다. Django의 초창기 몇 년 동안은 이 방식이 기본제공 방식이었다. 그러나 이는 개발자가 항상 신경을 써야한다는 문제점이 있다.
  • 다른 방법은 Django의 자동화된 HTML 회피 처리(auto-escaping)를 이용하는 것이다. Django는 기본적으로 다음과 같이 문자열들을 변환한다. (HTML에서 특수 문자를 글자처럼 표현할 수 있는 문자 참조를 활용하는 방식이다.)
    • <&lt;로 변환된다.
    • >&gt;로 변환된다.
    • '(single quote)는 &#39;로 변환된다.
    • "(double quote)는 &quot;로 변환된다.
    • &&amp;로 변환된다.

Auto-escaping을 끄는 방법

각각의 변수 처리 방식

safe 필터를 사용하는 방식이다.

치환된 결과입니다: 
치환되지 않은 결과입니다: 

만약 data 변수에 <b>라는 문자열이 할당되어 있다고 가정해보자. 위와 같은 템플릿 조각은 다음과 같은 결과로 렌더링 된다.

치환된 결과입니다: &lt;b&gt;
치환되지 않은 결과입니다: <b>
템플릿 블럭 처리 방식

전체 템플릿 혹은 특정 부분에서 auto-escaping 기능을 제어하기 위해서는 autoescape 태그를 활용할 수 있다.

{% autoescape off %}
    Hello {{ name }}
{% endautoescape %}

autoescape 태그는 on 혹은 off라는 태그를 인자로 받을 수 있다. 다음과 같은 템플릿에서 상단의 name 변수는 HTML auto-escaping이 이루어진다.

Auto-escaping is on by default. Hello {{ name }}
{% autoescape off %}
    This will not be auto-escaped: {{ data }}.
    Nor this: {{ other_data }}
    {% autoescape on %}
        Auto-escaping applies again: {{ name }}
    {% endautoescape %}
{% endautoescape %}

autoescape 태그는 다음 예제와 같이 includeblock 태그 내부도 제어할 수 있다.

# base.html
{% autoescape off %}
<h1>{% block title %}{% endblock %}</h1>
{% block content %}
{% endblock %}
{% endautoescape %}

# child.html
{% extends "base.html" %}
{% block title %}This & that{% endblock %}
{% block content %}{{ greeting }}{% endblock %}

필터 인자에서의 문자열 auto-escaping

필터의 인자는 문자열이 될 수 있으며, 이는 auto-escaping 되지 않는다. 이는 자동으로 safe 필터를 거치기 때문이다. 그 이유는 개발자가 직접 작성한 문자열은 직접 제어할 수 있기 때문이다. 이 것은 템플릿에서 직접 문자열을 작성할 때는, 다음과 같이 작성해야 한다는 의미이다.

3 &lt; 2

아래와 같은 방식으로 작성하는 것은 바람직하지 않다.

3 < 2

템플릿 로딩의 내부

템플릿은 특별히 지정된 템플릿 디렉토리에 저장해야 한다. Django는 템플릿 로딩 설정에 따라 템플릿 디렉토리를 탐색한다. 기본적인 방식은 DIRS 옵션을 설정하는 것이다.

DIRS 옵션

다음과 같은 방식으로 템플릿이 위치한 디렉토리를 지정해줄 수 있다.

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            '/home/html/templates/lawrence.com',
            '/home/html/templates/default',
        ],
    },
],

템플릿은 웹서버가 읽을 수 있는 곳(만약 Linux와 같은 시스템에서 웹서버를 구성한다면, 웹서버 프로세스를 처리하는 계정이 읽기 권한이 있는 위치에 템플릿이 있어야 한다.)이라면, 원하는 위치 어디에든 둘 수 있다. 또한 템플릿은 원하는 어떤 확장자를 사용해도 된다. .html, .txt 등과 같은 것이나 혹은 아얘 확장자가 없어도 된다. 주의할 점은 Windows여도 경로 지정은 Unix 스타일 방식의 슬래시여야 한다는 점이다.

템플릿 로더의 종류

Django는 기본적으로 파일시스템 템플릿 로더를 사용한다. 그러나 다른 방식의 템플릿 로더를 사용할 수도 있다.

Filesystem loader

filesystem.LoaderDIRS에 지정된 파일시스템으로부터 템플릿을 읽어온다. 다음과 같이 설정했다면, 프로젝트가 위치한 디렉토리 하위의 templates라는 디렉토리 하위에 있는 템플릿들을 읽어들일 수 있다.

TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [os.path.join(BASE_DIR, 'templates')],
}]
App directories loader

app_directories.Loader는 Django 앱들이 있는 파일시스템에서 템플릿을 읽어온다. INSTALLED_APPS에 설정된 앱들의 디렉토리 하위에 있는 templates 디렉토리를 찾게 된다. 만약 INSTALLED_APPS가 다음과 같이 설정되어 있다면,

INSTALLED_APPS = ['myproject.reviews', 'myproject.music']

get_template('foo.html')과 같은 코드를 실행했을 때, 다음 디렉토리를 순차적으로 탐색하다가 첫번째로 찾아낸 ‘foo.html’을 사용한다.

  • /path/to/myproject/reviews/templates/
  • /path/to/myproject/music/templates/ 따라서, INSTALLED_APPS에 지정된 앱들의 순서는 Django가 템플릿을 사용하게 되는데 영향을 끼칠 수 있다.

이 방식의 로더를 사용하기 위해서는 다음과 같이 APP_DIRS 옵션을 True로 설정해주어야 한다.

TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'APP_DIRS': True,
}]
다른 방식의 로더

아래와 같은 로더들이 제공되지만, 기본적으로 활성화되어 있지 않다.

  • django.template.loaders.eggs.Loader
  • django.template.loaders.cached.Loader
  • django.template.loaders.locmem.Loader

템플릿 시스템 확장하기

코드 레이아웃

커스텀 템플릿 태그와 필터는 Django app 내부에 있어야 한다. 이 것들을은 templatetags 디렉토리 안에 있어야 하며, 이 디렉토리는 models.pyviews.py 등과 동일한 레벨에 위치해야 한다. 또한 Python 패키지로 인식될 수 있도록, __init__.pytemplatetags 디렉토리 내에 있어야 한다. 만약 review_extras.py라는 파일에 커스텀 태그와 필터를 작성할 예정이라면, 앱이 다음과 같은 구조로 구성되어야 한다.

reviews/
    __init__.py
    models.py
    templatetags/
        __init__.py
        review_extras.py
    views.py

이렇게 구성된 이후, 템플릿에서 사용하기 위해서는 다음과 같이 로드할 수 있다.

{% load review_extras %}

또한, review_extras 모듈을 사용하기 위해서는 앱이 INSTALLED_APPS에 등록되어야 한다.

만약 필터나 태그에 대해 좀 더 알아보고 싶다면, Django의 기본 필터와 태그 코드가 어떻게 구현되어 있는지 확인해보면 도움이 된다. 이들은 django/template/defaultfilters.pydjango/template/defaulttags.py에 위치해 있으니, 확인해보자.

템플릿 라이브러리 만들기

템플릿 라이브러리를 만드는 것은 두 단계로 이루어진다.

  1. 템플릿 라이브러리 위치를 결정해야 한다. manage.py startapp으로 생성한 Django 앱에 둘 수도 있고, 다른 구성은 없는 단독 앱으로 만들 수도 있다. Django는 다른 프로젝트에서도 활용할 수 있기 때문에 후자를 추천한다. 어떤 구성으로 하건 INSTALLED_APPS에 추가하면 된다.
  2. templatetags 패키지를 생성해야 한다. 예를 들면 다음과 같다.

     my_template_library/
         __init__.py
         templatetags/
             __init__.py
             review_extras.py
    

상술했던 바와 같이 models.pyviews.py와 함께 있어도 되지만, 단독으로 templatetags 패키지만 있어도 무관하다. 태그 라이브러리로 인식되기 위해서는 register라는 이름을 가진 변수를 모듈에 선언하면 된다.

from django import template

register = template.Library()

커스텀 템플릿 태그와 필터

커스텀 템플릿 필터 작성하기

커스텀 필터는 다음과 같은 하나 혹은 두 개의 인자를 전달받는다.

  1. 변수의 값(input): 반드시 문자열일 필요는 없다.
  2. 필터의 인자 값: 기본 값으로 사용되거나 버려질 수 있다.

예를 들어 `` 라고 필터를 사용하게 되면, foo라는 필터는 var 변수의 값을 첫 번째 인자로 전달받고, “bar“라는 문자열을 두 번째 인자로 전달받는다. 템플릿 언어는 익셉션 핸들링을 제공하지 않기 때문에, 커스텀 필터 내에서 어떤 익셉션이 발생하게 되면 서버 에러로 노출될 수 있다. 그러므로 필터 함수는 반드시 익셉션을 발생시키지 않고 적절한 기본값을 반환하도록 작성해야 한다. 예를 들면, 다음과 같이 커스텀 필터를 작성할 수 있다.

def cut(value, arg):
    """Removes all values of arg from the given string"""
    return value.replace(arg, '')

또한 위에서 작성한 예제는 다음과 같이 활용할 수 있다.


대부분의 필터는 두 번째 인자를 받지 않는다. 이 경우, 두 번째 인자는 함수 내에 전달되지 않는다.

def lower(value): # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()

커스텀 필터 등록하기

직접 작성한 필터를 Django 템플릿언어에서 사용하기 위해서는 Library instance에 등록해야 한다.

register.filter('cut', cut)
register.filter('lower', lower)

Library.filter() 함수는 기본적으로 두 개의 인자를 받는다.

  1. 문자열로 된 필터의 이름
  2. Python 함수

혹은 decorator로 사용할 수도 있다.

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()

만약 위 예제의 두 번째와 같이 name 인자를 전달하지 않으면, 함수의 이름을 필터의 이름으로 사용한다. 또한 register.filter() 함수는 세 개의 키워드 인자를 받는다. is_safe, needs_autoescape, expects_localtime이 그것이다.

문자열만 처리하는 템플릿 필터

만약 첫 번째 인자로 문자열만을 받기를 원하는 필터를 작성한다면, stringfilter라는 데코레이터를 사용해야 한다. 이 데코레이터를 사용하면, 객체를 문자열 값으로 변환하여 필터 함수에 넘겨준다. 이는 다음과 같이 정의하여 사용할 수 있다.

from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def lower(value):
    return value.lower()

필터와 Auto-escaping

커스텀 필터를 작성할 때는, Django의 auto-escaping 동작방식을 고려해야 한다.

  • 가공되지 않은 문자열: 이것은 Python str 혹은 unicode 그대로의 타입이다.
  • 안전한 문자열: 안전하다고 표시된 문자열이다. Django 내부적으로 이러한 타입은 SafeBytes 혹은 SafeText 타입으로 관리되며, SafeData의 하위 클래스이다. 따라서 다음과 같이 검사하여 사용할 수 있다.

      if isinstance(value, SafeData):
          # Do something with the "safe" string.
          ...
    
    
  • escaping이 필요하다고 처리된 문자열: 최종 결과에서 항상 escaping 된다. autoescape 블럭 안에 있든 그렇지 않든 반드시 escaping 처리가 이루어 진다. EscapeBytes 혹은 EscapeText 타입으로 관리된다.