본문 바로가기
  • 실행력이 모든걸 결정한다
Django

[Django] 템플릿 시스템

by 김코더 김주역 2023. 1. 24.
반응형

1. 템플릿 시스템과 렌더링

- 템플릿 시스템은 템플릿 문법으로 작성된 템플릿 코드를 해석하여 HTML, XML, JSON 등의 파일로 결과물을 만들어주는 시스템이다.

- 템플릿 코드를 해석하는 과정을 렌더링이라고 한다.

- 장고의 코어 템플릿 엔진은 DTL(Django Template Language)이며, 기본적으로 지원되는 Jinja라는 템플릿 엔진도 사용한다. 다른 템플릿 엔진도 설치시 사용 가능하다.

 

 

 

2. 템플릿 시스템 관련 설정

- 팀플릿 시스템 관련 설정은 settings.py의 TEMPLATES 항목에서 한다.

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'utils.context_processors.get_timelist',
            ],
        },
    },
]

- BACKEND : 사용할 템플릿 엔진이다. Jinja2는 기본적으로 지원되며, DjangoTemplate라는 장고의 자체 템플릿 엔진을 사용하도록 설정 되어있다.

- DIRS : 프로젝트 템플릿 파일을 찾을 디렉토리다. 애플리케이션 템플릿 디렉토리보다 먼저 탐색된다.

- APP_DIRS : 애플리케이션 템플릿 파일을 찾을지 여부를 지정한다. 원래는 False였다가 startproject 명령에 의해 True로 바뀐다.

- OPTIONS : 템플릿 엔진에 따른 옵션 항목들을 설정한다. 장고 템플릿 엔진을 사용하는 경우는 다음과 같은 옵션들을 설정할 수 있다.

  • context_processors : 클라이언트 요청 객체인 request 인자를 받아서 공통 컨텍스트 데이터를 반환하는 함수 리스트다. 기본 설정들로 인해 debug, sql_queries, request, user, perms, messages, DEFAULT_MESSAGE_LEVELS 변수들을 사용할 수 있다. 위의 예시에서 맨 마지막에 있는 내용은 https://kimcoder.tistory.com/588에서 필자가 추가한 것이다.
  • debug : 템플릿 디버그 모드 여부로, 디폴트는 DEBUG 항목의 값이다. True로 지정되어 있으면 템플릿 파일 내에서 에러가 발생한 줄을 다른 색으로 표시한다.
  • loaders : 템플릿 파일을 메모리로 로딩해주는 템플릿 로더 클래스 리스트를 지정한다. 디폴트는 템플릿 디렉토리를 검색하는 django.template.loaders.filesystem.Loader와 애플리케이션 디렉토리를 검색하는 django.template.loaders.app_directories.Loader 2가지다.
  • string_if_invalid : 템플릿 변수가 잘못된 경우 대신 사용할 문자열을 의미하며, 디폴트는 공백 문자열이다.
  • file_charset :  템플릿 파일을 읽어 디코딩할 때 사용하는 문자셋을 의미하며, 디폴트는 'utf-8'이다.

 

 

 

3. 템플릿 변수

1) 형식

{{ variable }}

 

 

2) dot 표현식의 해석 순서

- 변수의 속성에 접근할 수 있는 dot(.) 표현식도 사용할 수 있는데, dot 표현식은 다음과 같은 순서로 해석이 이루어진다.

  • 변수가 딕셔너리 타입인지 확인하여 객체['속성']으로 해석
  • 객체.속성으로 해석
  • 변수가 리스트 타입인지 확인하여 객체[속성]으로 해석

 

 

3) 정의가 되어 있지 않은 변수를 사용하는 경우

- 기본적으로 빈 문자열로 채워준다.

- 채워줄 값을 변경하고 싶다면 settings.py의 TEMPLATES 항목에서 변경하면 된다. [2. 템플릿 시스템 관련 설정]의 string_if_invalid 옵션에서 언급한 내용이다.

 

 

4) 리스트를 사용하는 경우

- 템플릿 코드에서 대괄호 대신 dot(.)으로 리스트 이름과 인덱스를 이어주면 된다. 만약, myList[0]를 출력하고 싶다면 {{ myList.0 }}와 같이 작성하면 된다.

 

 

 

4. 템플릿 필터

- 템플릿 변수에 필터를 적용하여 변수의 출력 결과를 변경할 수 있다.

 

1) 형식

- 변수 옆에 파이프 문자를 사용하여 필터를 붙인다.

{{ variable|filter }}

- 필터를 체인으로 연결하여 여러 개의 필터를 적용할 수 있다.

{{ variable|filter|filter...}}

 

 

2) 필터의 종류

(1) lower : 변수값의 모든 문자를 소문자로 변경

(2) upper : 변수값의 모든 문자를 대문자로 변경

(3) escape : 변수값에 있는 특수 문자를 그 문자에 상응하는 HTML Entity로 변환

- 템플릿에서는 {% autoescape on %} 템플릿 태그를 지원한다. 아래 두 코드의 결과는 같다.

{{ content|escape }}

 

{% autoescape on %}
    {{ content }}
{% endautoescape %}

(4) linebreaks : 빈 줄은 <p> 태그로 감싸고, 개행 1개는 <br> 태그로 변환

(5) linebreaksbr : 모든 개행을 <br> 태그로 변환

(6) truncatewords:x : 변수값 중에서 앞에 x개의 단어만 보여주고, 줄 바꿈 문자는 모두 없앰

(7) join:str : 파이썬의 join 역할

{{ list|join:" // "}}

※ 필터의 인자에 빈칸이 있는 경우는 위와 같이 따옴표로 묶어줘야 함

(8) default:str : 변수값이 False이거나 없는 경우, str로 표시

(9) length : 변수값의 길이를 반환

(10) striptags : 모든 html 태그를 제거해서 반환

(11) pluralize:str : 값이 1이 아닐 경우, str을 반환

- 복수접미어를 나타내기 위한 목적으로 사용됨

- str은 생략 가능하며, 기본값은 's'임

(12) add:obj : 더하기 필터

- obj의 데이터 타입(숫자, 문자열, 리스트 등)에 따라 결과가 달라짐. 예를 들어, value값이 5라면 아래 표현식의 최종 결과는 8이 됨

{{ value|add:"3" }}

- 처음에는 value, add가 모두 정수 타입이라고 간주하고 덧셈을 시도하는데, 이 시도가 실패하면 타입이 허용하는 문법에 따라 더하기를 시도함. 이 시도도 실패하면 빈 문자열을 반환함

 

+) 다른 수많은 필터들은 아래 공식문서에서 확인할 수 있다.

https://docs.djangoproject.com/en/4.1/ref/templates/builtins/#built-in-filter-reference

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

 

 

5. 템플릿 태그

1) {% for %}

- 반복 가능한 객체에 담겨 있는 항목들을 순회

- for 태그 내에서 사용할 수 있는 다양한 변수를 제공한다.

  • forloop.counter : 1부터 루프 카운트
  • forloop.counter0 : 0부터 루프 카운트
  • forloop.revcounter : 루프 끝에서 현재가 몇 번째인지 1부터 카운트
  • forloop.revcounter0 : 루프 끝에서 현재가 몇 번째인지 0부터 카운트
  • forloop.first : 루프에서 첫 번째 실행이면 True
  • forloop.last : 루프에서 마지막 실행이면 True
  • forloop.parentloop : 부모 루프를 참조 - 예) forloop.parentloop.counter

 

예시

<ul>
{% for fruit in fruit_list %}
    <li>{{forloop.counter}} {{fruit.name}}</li>
{% endfor %}
</ul>

 

 

2) {% if %}

- 조건을 검사하여 True면 아래 문장을 표시하고, False면 아래 블록으로 내려감

- if, elif, else 태그 순서로 구성되며, elif 태그는 생략하거나 연속으로 여러 개를 사용할 수 있음

{% if 조건문1 %}
    ...
{% elif 조건문2 %}
    ...
{% else %}
    ...
{% endif %}

- 조건문에 필터와 연산자(and, or, not, and not, in, not in, ==, !=, <, >, <=, >=)를 사용할 수 있다.

 

 

3) {% csrf_token %}

- form을 POST방식으로 전송할 때, 해커가 데이터를 위조하지 못하도록 보호해주는 키워드

- 다음 예시와 같이 <form>의 첫 줄 다음에 넣는다.

<form action="{% url 'users:index' %}" method="POST">
  {% csrf_token %}
  n : <input type="text" name="n"><br>
  a : <input type="text" name="a"><br>
  <input type="submit" value="제출">
</form>

- 이 태그를 붙이면 장고가 CSRF 토큰값의 유효성을 검증하며, 검증에 실패하면 403 에러를 띄움

- CSRF 토큰값이 유출되지 않도록, 외부 URL로 보내는 <form>에는 사용하지 않는 것이 좋다.

※ CSRF(Cross Site Request Forgery) : 특정 웹 사이트에서 이미 인증을 받은 사용자가 공격 코드가 삽입된 페이지를 열도록 하는 공격

 

 

4) {% url %}

- 템플릿 소스에 URL을 하드코딩하는 것을 방지하기 위한 태그로, 나중에 URL이 변경되더라도 템플릿 소스는 수정할 필요가 없도록 할 수 있다.

- 태그의 사용 형식은 다음과 같다.

{% url 'namespace:view-name' arg1 arg2 ... %}
  • namespace : urls.py의 include() 함수 또는 app_name에 정의한 이름
  • view-name : urls.py에서 정의한 URL 패턴 이름
  • argN : view 함수에서 사용하는 N번째 인자. 인자가 없다면 생략하고, 여러 개인 경우 빈칸으로 구분함

 

 

5) {% with %}

- with 구문 내에서 특정 값을 변수에 저장할 때 쓰인다. 아래 예시에 있는 두 문법 모두 사용 가능하다.

{% with cnt=company.employees.count %}
    {{ cnt }} people are working!
{% endwith %}

 

{% with company.employees.count as cnt %}
    {{ cnt }} people are working!
{% endwith %}

 

 

6) {% load %}

- 사용자 정의 태그 및 필터를 로딩

- 아래 예시에서는 somelib.py, package/otherlib.py 파일에 정의된 사용자 정의 태그 및 필터를 로딩해준다.

{% load somelib package.otherlib %}

 

 

7) {% include %}

- 공통된 코드를 재활용하면서 코드 중복을 줄이기 위해 {% extends %} ([8. 템플릿 상속]에서 설명할 것) 다음으로 많이 쓰이는 태그로, 다른 템플릿 파일을 현재의 템플릿 파일에 포함시키고 싶을 때 쓰인다. 다른 템플릿 파일에 템플릿 변수가 있다면, 이 템플릿 파일을 사용하는 현재의 뷰에서 제공하는 컨텍스트 변수를 사용한다.

<div>
  {% include "foo/bar.html" %}
</div>

- with을 사용하면 키워드 인자를 통해 템플릿 변수값을 지정할 수 있다. 뒤에 only를 붙이면 현재의 뷰에서 제공하는 컨텍스트 변수와 상관 없이 해당 변수값으로 고정된다.

<div>
  {% include 'introduce.html' with person='Jooyeok' greeting='Hello' only}
</div>

 

{% include %} 태그의 인자로는 위의 예시와 같이 문자열을 줄 수도 있지만, 변수로 주는 방법도 있다는 점을 참고해두자.

 

+) 다른 수많은 태그들은 아래 공식문서에서 확인할 수 있다.

https://docs.djangoproject.com/en/4.1/ref/templates/builtins/#built-in-tag-reference

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

 

 

6. 템플릿 주석

1) 한 줄 주석문

- {# #} 형식을 사용한다.

- '{#'과 '#}' 으로 감싸진 모든 내용은 주석 처리된다.

 

 

2) 여러 줄 주석문

- {% comment %} 태그를 사용한다. 문구는 없어도 되지만, 주석 처리 사유를 기록해둬도 나쁘지 않다.

{% comment "주석 처리 사유 문구" %}
    ...
{% endcomment %}

- {% comment %} 태그는 중첩하여 사용할 수 없다.

 

 

 

7. HTML 이스케이프

- 렌더링 시 템플릿 변수에 HTML 태그가 들어있으면 원하지 않는 결과가 나올 수도 있다. 또, 이러한 취약점을 이용하여 누군가가 악성 스크립트로 공격(XSS)을 할 수도 있다. 그래서 장고는 자동 이스케이프 기능을 제공하고 있다.

※ <, >, ', ", &을 각각 &lt;, &gt;, &#39;, &quot;, &amp로 변환함

- 필터의 인자에 사용되는 스트링 리터럴에는 자동 이스케이프 기능이 적용되지 않기 때문에, 가급적 수동으로 변환해주자.

※ 예) data|default:"3 < 5" 대신 data|default:"3 &lt; 5" 사용

 

1) HTML 이스케이프 활성

- escape 필터 사용

- {% autoescape on %}, {% endautoescape %}로 활성화 범위 설정

※ 이 포스팅의 [3-2)-(3) escape] 참고

 

 

2) HTML 이스케이프 비활성

- safe 필터 사용

- {% autoescape off %}, {% endautoescape %}로 비활성화 범위 설정

 

 

 

8. 템플릿 상속

- 템플릿 상속을 통해 템플릿 코드를 재사용할 수 있다. 자식 템플릿의 맨 윗줄에 {% extends %} 태그를 사용하여 상속받을 부모 템플릿을 지정한다.

{% extends "base.html" %}

- 부모 템플릿에서는 템플릿의 뼈대를 만들어두고, 자식 템플릿이 재정의할 수 있는 부분은 {% block %} 태그로 지정해둔다.

{% block name %}Parent contents{% endblock %}

- 자식 템플릿은 부모 템플릿의 뼈대를 그대로 재사용하면서 {% block %} 부분을 재정의할 수 있다. {% block %} 부분을 작성하지 않으면 부모 템플릿의 {% block %} 부분을 그대로 재사용한다.

{% block name %}Child contents{% endblock %}

 

예시

[부모 템플릿 - base.html]

- {% block title %}, {% block extra-style%}, {% block content%}, {% block footer%}, {% block extra-script%} 부분을 자식 템플릿에서 재정의할 수 있다.

{% load static %}

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        <meta name="description" content="" />
        <meta name="author" content="" />
        <title>{% block title %}{% endblock %}</title>
        <!-- Favicon-->
        <link rel="icon" type="image/x-icon" href="{% static 'assets/favicon.ico' %}" />
        <!-- Font Awesome icons (free version)-->
        <script src="https://use.fontawesome.com/releases/v6.1.0/js/all.js" crossorigin="anonymous"></script>
        <!-- Google fonts-->
        <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css" />
        <link href="https://fonts.googleapis.com/css?family=Roboto+Slab:400,100,300,700" rel="stylesheet" type="text/css" />
        <!-- Core theme CSS (includes Bootstrap)-->
        <link href="{% static 'css/styles.css' %}" rel="stylesheet" />
        {% block extra-style %}{% endblock %}
    </head>
    <body id="page-top">
        <nav class="navbar navbar-expand-lg navbar-dark fixed-top" id="mainNav">
            <div class="container">
                <div class="navbar-brand">Vue Django Programming</div>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
                    Menu
                    <i class="fas fa-bars ms-1"></i>
                </button>
                <div class="collapse navbar-collapse" id="navbarResponsive">
                    <ul class="navbar-nav text-uppercase ms-auto py-4 py-lg-0">
                        <li class="nav-item"><a class="nav-link" data-bs-target="#page-top" href="{% url 'home' %}#page-top">Home</a></li>
                        <li class="nav-item"><a class="nav-link" data-bs-target="#portfolio" href="{% url 'home' %}#portfolio">Blog</a></li>
                        <li class="nav-item"><a class="nav-link" data-bs-target="#team" href="{% url 'home' %}#team">Iam</a></li>
                        <li class="nav-item"><a class="nav-link" href="{% url 'admin:index' %}">Admin</a></li>
                    </ul>
                </div>
            </div>
        </nav>
        <header class="masthead">
            <div class="container">
                <div class="masthead-subheading">Welcome To Our Blog!</div>
                <div style="margin-bottom: 250px;">나의 일상 생활</div>
            </div>
        </header>

        {% block content %}{% endblock %}

        {% block footer %}{% endblock %}

        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        <script src="{% static 'js/scripts.js' %}"></script>
        <script src="https://cdn.startbootstrap.com/sb-forms-latest.js"></script>
        {% block extra-script %}{% endblock %}
    </body>
</html>

 

[자식 템플릿 - home.html]

- {% extends %} 태그를 통해 상속받을 템플릿을 지정했다.

- {% block title %}, {% block extra-style%}, {% block content%}, {% block footer%}, {% block extra-script%} 부분을 모두 재정의했다. 코드가 너무 길어서 일부는 ...으로 생략했다.

{% extends 'base.html' %}
{% load static %}

{% block title %}home.html{% endblock %}

{% block content %}
    <section class="page-section bg-light" id="portfolio">
        ...
    </section>
    <section class="page-section bg-light" id="team">
        ...
    </section>
{% endblock %}

{% block footer %}
    <footer class="footer py-4">
        ...
    </footer>
{% endblock %}

{% block extra-script %}
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
        const PostListApp = {
            delimiters: ...,
            data() {
                return {
                    ...
                }
            },
            created() {
                ...
            },
            methods: {
                ...
            }
        }
    </script>
{% endblock %}

위와 같이 작성하면 자식 템플릿은 기본적으로 부모 템플릿 코드를 모두 사용하면서 {% block %} 태그로 재정의한 부분만 바뀌는 형태가 된다.

- {% block %} 태그 내에서 {{ block.super }} 변수를 통해 부모 템플릿의 {% block %} 내용을 그대로 가져오고 다른 내용을 추가할 수도 있다.

# Template: A.html
<html>
    <head></head>
    <body>
        {% block hello %}
            HELLO 
        {% endblock %}
    </body>
</html>



# Template B.html
{% extends "A.html" %}
{% block hello %}
World
{% endblock %}

# Rendered Template B
<html>
    <head></head>
    <body>
World
    </body>
</html>



# Template C
{% extends "A.html" %}
{% block hello %}
{{ block.super }} World
{% endblock %}

# Rendered Template C
<html>
    <head></head>
    <body>
Hello World
    </body>
</html>

※ 출처 : https://gist.github.com/issackelly/928783

- 가독성을 위해 {% endblock %} 에도 블록 이름을 붙일 수 있다.

{% endblock name %}

 

 

 

9. 템플릿 처리 내부 동작

i) Engine 객체 생성 : settings.py의 TEMPLATES 항목들을 인자로 사용하여 Engine 객체를 생성한다.

 

ii) Loader 객체 생성 : Engine 객체에 소속된 Loader 객체를 생성한다.

 

iii) Template 객체 생성 : Loader 객체들은 템플릿 파일들을 찾은 후에 Template 객체를 생성한다. {% extends %} 또는 {% include %} 태그를 이용해서 나누어놓은 템플릿 파일들이 있다면 이들을 생성자에 모아서 Template 객체를 생성한다. 템플릿 파일은 우선적으로 프로젝트 템플릿 디렉토리에서 검색되고, 그 다음으로 settings.py의 INSTALLED_APPS 항목에 지정된 앱의 순서에 따라 애플리케이션 템플릿 디렉토리에서 검색된다.

 

iv) Template 객체의 render() 함수를 이용하여 컨텍스트 데이터와 요청 데이터를 템플릿 코드에 대입해서 렌더링을 진행한다. 뷰에서 전달된 데이터만으로 최종 컨텍스트 데이터를 만들 때는 Context 객체를 사용하고, HttpRequest 데이터를 포함해 최종 컨텍스트 데이터를 만들 때는 RequestContext 객체를 사용한다. 어떤 데이터를 컨텍스트 데이터로 사용할지는 [2. 템플릿 시스템 관련 설정]에서 살펴봤던 context_processors 옵션에 따라 결정된다. 참고로, RequestContext 객체가 생성되면 django.template.context_processors.csrf 프로세서가 자동으로 추가되어 {% csrf_token %} 태그 처리에 필요한 토큰도 최종 컨텍스트 데이터에 추가된다.

 

v) 렌더링 결과로 최종 HTML 텍스트 파일이 생성된다.

 

 

 

10. 제네릭 뷰의 디폴트 템플릿명

- 모델을 대상으로 로직을 처리하는 제네릭 뷰들은 대부분 디폴트 템플릿명을 가진다. 제네릭 뷰의 template_name 속성을 지정하지 않았다면 디폴트 템플릿명을 사용한다.

- 아래 포스팅의 [5. 제네릭 뷰의 종류]에서 각 제네릭 뷰별 디폴트 템플릿명을 확인할 수 있다. 디폴트 템플릿명을 갖지 않는 제네릭 뷰도 있다.

https://kimcoder.tistory.com/581/

 

[Django] 클래스형 뷰와 제네릭 뷰 소개

장고에서는 뷰를 클래스로도 작성할 수 있으며, 함수형 뷰보다는 클래스형 뷰가 더 이점이 많다. 이번 포스팅에서는 클래스형 뷰와 제네릭 뷰에 대한 감을 잡아보자. 1. 클래스형 뷰의 장점 - 다

kimcoder.tistory.com

 

반응형

댓글