발행일

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)은 써보지 않아 다루지 않았습니다.