템플릿 (Jinja) — 자동 이스케이프와 컨텍스트 프로세서

템플릿 (Jinja) — 자동 이스케이프와 컨텍스트 프로세서

Flask는 템플릿 엔진으로 Jinja를 써요. 물론 다른 템플릿 엔진을 쓰는 건 자유지만, Flask를 실행하려면 Jinja를 설치해야 해요. 이 요구사항은 풍부한 확장을 가능하게 하는 데 필요해요. 확장이 Jinja가 존재한다는 것에 의존할 수 있거든요.

이 섹션은 Jinja가 Flask에 어떻게 통합되는지 아주 간단히 소개할게요. 템플릿 엔진 문법 자체에 대한 정보는 공식 Jinja 템플릿 문서를 봐요.

출처: 공식문서 - Templates

본문

Jinja 설정

커스터마이즈하지 않는 한 Flask는 Jinja를 다음과 같이 설정해요.

  • :func:~flask.templating.render_template을 쓸 때 .html, .htm, .xml, .xhtml, 그리고 .svg로 끝나는 모든 템플릿에는 자동 이스케이프(autoescaping)가 켜져 있어요.
  • :func:~flask.templating.render_template_string을 쓸 때 모든 문자열에는 자동 이스케이프가 켜져 있어요.
  • 템플릿은 {% autoescape %} 태그로 자동 이스케이프를 켜고 끌 수 있어요.
  • Flask는 기본으로 존재하는 값들에 더해 몇 가지 전역 함수와 헬퍼를 Jinja 컨텍스트에 넣어요.

표준 컨텍스트

기본적으로 Jinja 템플릿 안에서 다음 전역 변수를 쓸 수 있어요.

  • config — 현재 설정 객체(:data:flask.Flask.config).
  • request — 현재 요청 객체(:class:flask.request). 활성 요청 컨텍스트 없이 템플릿을 렌더링하면 이 변수는 쓸 수 없어요.
  • session — 현재 세션 객체(:class:flask.session). 활성 요청 컨텍스트 없이 렌더링하면 이 변수는 쓸 수 없어요.
  • g — 요청에 묶인 전역 변수용 객체(:data:flask.g). 활성 요청 컨텍스트 없이 렌더링하면 이 변수는 쓸 수 없어요.
  • url_for:func:flask.url_for 함수.
  • get_flashed_messages:func:flask.get_flashed_messages 함수.

Jinja 컨텍스트 동작에 대해 알아둘 게 있어요. 이 변수들은 변수 컨텍스트에 추가되는 것이지 전역 변수가 아니에요. 따라서 기본적으로 임포트된 템플릿의 컨텍스트에는 나타나지 않아요. 부분적 으로는 성능 고려 때문이고, 부분적으로는 명시성을 유지하기 위해서예요.

이것이 당신에게 무엇을 의미할까요? 임포트하려는 매크로가 request 객체에 접근해야 한다면 두 가지 방법이 있어요.

  1. request를 매크로에 파라미터로, 또는 관심 있는 request 객체의 속성으로 명시적으로 전달해요.
  2. 매크로를 "with context"로 임포트해요.

context로 임포트하는 건 이렇게 생겼어요.

{% from '_helpers.html' import my_macro with context %}

자동 이스케이프 제어

자동 이스케이프는 특수 문자를 자동으로 이스케이프해 주는 개념이에요. HTML(또는 XML, 그래서 XHTML)의 의미에서 특수 문자는 &, >, <, ", 그리고 '예요. 이 문자들은 문서에서 그 자체로 특정 의미를 지니기 때문에 텍스트로 쓰려면 "엔티티(entity)"로 바꿔야 해요. 그렇게 하지 않으면 사용자가 텍스트에서 이 문자를 쓸 수 없게 돼 불편할 뿐 아니라 보안 문제로도 이어질 수 있어요.

하지만 때로는 템플릿에서 자동 이스케이프를 꺼야 할 때가 있어요. 예를 들어 마크다운을 HTML로 변환하는 시스템처럼 안전한 HTML을 생성하는 시스템에서 온 HTML을 페이지에 명시적으로 주입하고 싶을 때예요.

이를 하는 방법은 세 가지예요.

  • 파이썬 코드에서 HTML 문자열을 :class:~markupsafe.Markup 객체로 감싸 템플릿에 전달해요. 이것이 일반적으로 권장되는 방법이에요.
  • 템플릿 안에서 |safe 필터로 문자열을 안전한 HTML로 명시적으로 표시해요 ({{ myvariable|safe }}).
  • 자동 이스케이프 시스템을 아예 임시로 꺼요.

템플릿에서 자동 이스케이프 시스템을 끄려면 {% autoescape %} 블록을 써요.

{% autoescape false %}
    <p>autoescaping is disabled here
    <p>{{ will_not_be_escaped }}
{% endautoescape %}

이렇게 할 때는 그 블록 안에서 쓰는 변수에 아주 주의하세요.

필터·테스트·전역 등록

Flask 앱과 블루프린트는 Jinja 템플릿에서 쓸 자신만의 필터·테스트·전역 함수를 등록하는 데코레이터와 메서드를 제공해요. 모두 같은 패턴을 따르므로 다음 예시는 필터만 다뤄요.

함수를 :meth:~.Flask.template_filter로 데코레이팅하면 템플릿 필터로 등록돼요.

@app.template_filter
def reverse(s):
    return reversed(s)
{% for item in data | reverse %}
{% endfor %}

기본적으로 함수 이름이 필터 이름으로 쓰이지만, 데코레이터에 이름을 전달해서 바꿀 수 있어요.

@app.template_filter("reverse")
def reverse_filter(s):
    return reversed(s)

:meth:~.Flask.add_template_filter로 필터를 별도로 등록할 수도 있어요. 이름은 선택 사항이고 주지 않으면 함수 이름을 써요.

def reverse_filter(s):
    return reversed(s)

app.add_template_filter(reverse_filter, "reverse")

템플릿 테스트는 :meth:~.Flask.template_test 데코레이터나 :meth:~.Flask.add_template_test 메서드를 써요. 템플릿 전역 함수는 :meth:~.Flask.template_global 데코레이터나 :meth:~.Flask.add_template_global 메서드를 써요.

같은 메서드들이 :class:.Blueprint에도 존재해요. 단 app_ 접두사가 붙는데, 등록된 함수가 블루프린트 안에서만이 아니라 모든 템플릿에서 쓸 수 있게 됨을 뜻해요.

Jinja 환경도 :attr:~.Flask.jinja_env로 접근할 수 있어요. Flask 밖에서 Jinja를 쓸 때처럼 직접 수정할 수 있어요.

컨텍스트 프로세서

템플릿의 컨텍스트에 새 변수를 자동으로 주입하려면 Flask에 컨텍스트 프로세서가 있어요. 컨텍스트 프로세서는 템플릿이 렌더링되기 전에 실행되며 템플릿 컨텍스트에 새 값을 주입할 수 있어요. 딕셔너리를 반환하는 함수예요.

@app.context_processor
def inject_user():
    return dict(user=g.user)

위 컨텍스트 프로세서는 g.user의 값을 가진 user라는 변수를 템플릿에서 쓸 수 있게 해줘요. 이 예시는 별로 흥미롭지 않아요. g는 어차피 템플릿에서 쓸 수 있으니까요. 그래도 어떻게 동작하는지 감을 잡을 수 있어요.

변수는 값에만 국한되지 않아요. 컨텍스트 프로세서는 함수도 템플릿에서 쓸 수 있게 해줘요 (파이썬은 함수를 전달할 수 있으니까요).

@app.context_processor
def utility_processor():
    def format_price(amount, currency="€"):
        return f"{amount:.2f}{currency}"
    return dict(format_price=format_price)

위 컨텍스트 프로세서는 format_price 함수를 모든 템플릿에서 쓰게 해줘요.

{{ format_price(0.33) }}

format_price를 템플릿 필터로 만들 수도 있지만, 이 예시는 컨텍스트 프로세서에서 함수를 전달하는 방법을 보여줘요.

스트리밍

전체 템플릿을 하나의 완전한 문자열로 렌더링하는 대신 스트림으로 렌더링해 더 작은 증분 문자열을 만드는 게 유용할 때가 있어요. 이는 HTML을 청크로 스트리밍해서 초기 페이지 로드를 빠르게 하거나 아주 큰 템플릿을 렌더링할 때 메모리를 아끼는 데 쓸 수 있어요.

Jinja 템플릿 엔진은 템플릿을 조각별로 렌더링해 문자열 iterator를 반환하는 걸 지원해요. Flask는 :func:~flask.stream_template:func:~flask.stream_template_string 함수를 제공해서 이걸 더 쉽게 쓸 수 있게 해요.

from flask import stream_template

@app.get("/timeline")
def timeline():
    return stream_template("timeline.html")

이 함수들은 요청이 활성 상태면 자동으로 :func:~flask.stream_with_context 래퍼를 적용해서 :data:.request, :data:.session, :data:.g가 템플릿에서 계속 쓸 수 있게 해요.

본문이 시작된 뒤에는 더 이상 헤더를 보낼 수 없어요. 따라서 응답을 시작하기 전에 모든 헤더를 설정했는지 확인해야 해요. 특히 템플릿이 session에 접근한다면 뷰에서도 접근해서 Vary: cookie 헤더가 설정되도록 하세요.

더 알아보기