- 발행일
Jinja 템플릿 정리 — 문법 레퍼런스와 Django에서 실제로 쓰며 배운 것
Jinja 템플릿 정리
Jinja는 HTML, XML 등 텍스트 파일을 동적으로 생성하는 템플릿 엔진입니다. Django 기본 템플릿(DTL) 대신 Jinja2를 쓰는 프로젝트에 들어가면서 정리했던 레퍼런스이고, 실제로 쓰며 배운 것들을 뒤에 붙였습니다.
공식 문서: https://jinja.palletsprojects.com/en/stable/templates/
기본 구분자
| 구분자 | 용도 |
|---|---|
{{ ... }} | 변수·표현식 출력 |
{% ... %} | 조건문·반복문 등 로직 |
{# ... #} | 주석 (출력 안 됨) |
{% if user.is_admin %}
관리자입니다.
{% else %}
일반 사용자입니다.
{% endif %}
{% for post in posts %}
<li>{{ post.title }}</li>
{% else %} {# 반복 대상이 비었을 때 — for의 else는 Jinja의 좋은 기능 #}
게시물이 없습니다.
{% endfor %}
심화 문법
| 문법 | 설명 |
|---|---|
{% set total = price * count %} | 변수 생성 |
{% include 'header.html' %} | 템플릿 포함 |
{% extends %} + {% block %} | 상속·블록 재정의 |
{% macro %} / {% call %} | 매크로 정의·호출 |
{% import 'macros.html' as m %} | 매크로 임포트 |
{% from 'macros.html' import input %} | 특정 매크로만 |
{% with %} | 로컬 변수 스코프 |
{% autoescape %} | 이스케이프 제어 |
{% do mylist.append(3) %} | 출력 없는 실행 |
자주 쓰는 필터
| 필터 | 예시 |
|---|---|
length | {{ items | length }} |
upper / lower | {{ name | upper }} |
replace | {{ s | replace('old', 'new') }} |
default | {{ nickname | default('이름 없음') }} |
truncate | {{ body | truncate(30) }} |
join | {{ tags | join(', ') }} |
striptags | {{ html | striptags }} |
safe | {{ trusted_html | safe }} — 아래 실무 절 필독 |
상속·매크로·공백 제어
{# base.html #}
<title>{% block title %}기본 제목{% endblock %}</title>
<body>{% block content %}{% endblock %}</body>
{# child.html #}
{% extends 'base.html' %}
{% block title %}홈페이지{% endblock %}
{% macro input(name, type='text') %}
<input name="{{ name }}" type="{{ type }}">
{% endmacro %}
{{ input('email', 'email') }}
<ul>
{%- for item in items -%} {# '-'로 앞뒤 공백·개행 제거 #}
<li>{{ item }}</li>
{%- endfor -%}
</ul>
루프 안에서는 loop.index(1부터), loop.first, loop.last를 쓸 수 있습니다.
실무에서 배운 것
0. 고백 — 이 글의 필터 표부터 틀려 있었다
원래 버전의 필터 표는 예시를 {{ upper }}, {{ length }}처럼 적어놨었습니다. 파이프 없이 필터 이름만 쓴, 실행되지 않는 문법이에요. 값 | 필터 구조인데 값을 통째로 빼먹은 거죠. 표로 "정리"하는 데 집중하면 정작 예시를 실행해 볼 생각을 안 하게 되는데, 돌려보지 않은 예시는 정리가 아니라 장식이었습니다. 이번에 전부 실행 가능한 형태로 고쳤습니다.
1. Django에서 Jinja2는 url·static이 공짜가 아니다
DTL의 {% url %}·{% static %} 태그는 Jinja2에 없습니다. Environment를 직접 만들어 전역 함수로 등록해야 해요.
# jinja2env.py
from django.urls import reverse
from django.templatetags.static import static
from jinja2 import Environment
def environment(**options):
env = Environment(**options)
env.globals.update({'url': reverse, 'static': static})
return env
이걸 해야 템플릿에서 {{ url('std-data:list') }} 형태를 쓸 수 있습니다. DTL 예제를 복사해 넣으면 태그 문법부터 달라서 깨지는데({% url 'name' %} vs {{ url('name') }}), 두 템플릿 언어가 "비슷해 보여서" 생기는 혼동이 초기에 잦았습니다.
2. safe는 편의 기능이 아니라 방어선 해제 스위치다
표에는 한 줄로 적혀 있지만, 실무에서 겪은 사고 후보는 전부 여기서 나왔습니다. 사용자 입력을 | safe로 렌더한 화면은 stored XSS 경로가 됐고, 서버 JSON을 | safe로 템플릿 리터럴에 주입한 코드는 데이터에 백틱 하나만 들어와도 깨지는 구조였어요. 정리하면 — safe를 쓰기 전에 물을 질문은 "이게 편한가"가 아니라 "이 값의 생산자를 신뢰하는가" 이고, JSON 전달은 safe가 아니라 json_script 계열이 정석입니다.
3. 매크로는 "공용 컴포넌트"의 서버사이드 버전이었다
프런트 프레임워크의 컴포넌트처럼, 반복되는 화면 조각(페이지네이션, 폼 행, 배지)은 매크로로 뽑아 {% from %}으로 가져다 썼습니다. 다만 매크로가 커지면 템플릿 파일이 로직 덩어리가 되는 건 컴포넌트와 똑같아서, 공용 매크로를 어디까지 허용할지 선을 그어야 했어요 — 실제 프로젝트에서의 정리 과정을 따로 남겼습니다.
한계
여기 정리한 건 Django + Jinja2 서버 렌더링 기준입니다. Flask에서의 컨텍스트 주입 방식이나 비동기 렌더링(enable_async)은 써보지 않아 다루지 않았습니다.