템플릿 엔진을 처음부터 건설하는 법 (Jinja2 또는 Django 템플릿처럼)
Daniel Hayes
Full-Stack Engineer · Leapcell

간단한 템플릿 엔진의 구현 및 원리 분석
간단한 템플릿 엔진을 작성하고 그 기본 구현 메커니즘을 심층적으로 살펴볼 것입니다.
언어 디자인
이 템플릿 언어의 디자인은 매우 기본적인데, 주로 두 가지 유형의 태그인 변수 태그와 블록 태그를 사용합니다.
변수 태그
변수 태그는 {{
및 }}
를 식별자로 사용합니다. 다음은 코드 예제입니다.
// 변수는 `{{` 및 `}}`를 식별자로 사용합니다. <div>{{template_variable}}</div>
블록 태그
블록 태그는 {%
및 %}
를 식별자로 사용합니다. 대부분의 블록은 종료 태그 {% end %}
를 사용하여 끝맺어야 합니다. 다음은 예제입니다.
// 블록은 `{%` 및 `%}`를 식별자로 사용합니다. {% each item_list %} <div>{{current_item}}</div> {% end %}
이 템플릿 엔진은 기본적인 루프 및 조건문을 처리할 수 있으며, 블록 내에서 호출 가능한 객체 호출도 지원합니다. 템플릿에서 Python 함수를 매우 편리하게 호출할 수 있습니다.
루프 구조
루프 구조는 컬렉션 또는 반복 가능한 객체를 반복하는 데 사용할 수 있습니다. 예제 코드는 다음과 같습니다.
// people 컬렉션 반복 {% each person_list %} <div>{{current_person.name}}</div> {% end %} // [1, 2, 3] 리스트 반복 {% each [1, 2, 3] %} <div>{{current_num}}</div> {% end %} // records 컬렉션 반복 {% each record_list %} <div>{{..outer_name}}</div> {% end %}
위의 예에서 person_list
등은 컬렉션이고 current_person
등은 현재 반복되는 요소를 가리킵니다. 점으로 구분된 경로는 딕셔너리 속성으로 구문 분석되며, ..
를 사용하여 외부 컨텍스트의 객체에 액세스할 수 있습니다.
조건문
조건문의 논리는 비교적 직관적입니다. 이 언어는 if
및 else
구조와 ==
, <=
, >=
, !=
, is
, <
, >
와 같은 연산자를 지원합니다. 예는 다음과 같습니다.
// num 값에 따라 다른 콘텐츠 출력 {% if num > 5 %} <div>5보다 큼</div> {% else %} <div>5보다 작거나 같음</div> {% end %}
호출 블록
호출 가능한 객체는 템플릿 컨텍스트를 통해 전달될 수 있으며 일반적인 위치 인수 또는 명명된 인수를 사용하여 호출할 수 있습니다. 호출 블록은 end
를 사용하여 닫을 필요가 없습니다. 예는 다음과 같습니다.
// 일반 인수 사용 <div class='date'>{% call format_date date_created %}</div> // 명명된 인수 사용 <div>{% call log_message 'here' verbosity='debug' %}</div>
컴파일 원리 및 프로세스
1단계: 템플릿 토큰화(tokenize)
원리
템플릿 토큰화는 컴파일의 시작 단계이며, 핵심 목표는 템플릿 콘텐츠를 독립적인 조각으로 나누는 것입니다. 이러한 조각은 일반 HTML 텍스트일 수도 있고, 템플릿에 정의된 변수 태그나 블록 태그일 수도 있습니다. 수학적으로 이는 복잡한 문자열을 분할하여 특정 규칙에 따라 여러 하위 문자열로 나누는 것과 유사합니다.
구현
정규 표현식과 split()
함수를 사용하여 텍스트 분할을 완료합니다. 다음은 구체적인 코드 예제입니다.
import re # 변수 태그의 시작 및 종료 식별자 정의 VAR_TOKEN_START = '{{' VAR_TOKEN_END = '}}' # 블록 태그의 시작 및 종료 식별자 정의 BLOCK_TOKEN_START = '{%' BLOCK_TOKEN_END = '%}' # 변수 태그 또는 블록 태그 일치를 위한 정규 표현식 컴파일 TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" % ( VAR_TOKEN_START, VAR_TOKEN_END, BLOCK_TOKEN_START, BLOCK_TOKEN_END ))
TOK_REGEX
정규 표현식의 의미는 변수 태그 또는 블록 태그를 일치시켜 텍스트 분할을 달성하는 것입니다. 표현식의 가장 바깥쪽 괄호는 일치하는 텍스트를 캡처하는 데 사용되며, ?
는 비 탐욕적 일치를 나타내어 정규 표현식이 첫 번째 일치에서 멈추도록 합니다. 예는 다음과 같습니다.
# 정규 표현식의 분할 효과를 실제로 보여줍니다. >>> TOK_REGEX.split('{% each vars %}<i>{{it}}</i>{% endeach %}') ['{% each vars %}', '<i>', '{{it}}', '</i>', '{% endeach %}']
그 후, 각 조각은 Fragment
객체로 캡슐화되며, 이는 조각의 유형을 포함하고 컴파일 함수의 매개변수로 사용될 수 있습니다. 총 4가지 유형의 조각이 있습니다.
# 조각 유형 상수 정의 VAR_FRAGMENT = 0 OPEN_BLOCK_FRAGMENT = 1 CLOSE_BLOCK_FRAGMENT = 2 TEXT_FRAGMENT = 3
2단계: 추상 구문 트리(AST) 구축
원리
추상 구문 트리(AST)는 소스 코드를 구조화된 방식으로 표현하는 데이터 구조로, 코드의 구문 구조를 트리 형태로 나타냅니다. 템플릿 컴파일에서 AST를 구축하는 목적은 토큰화에서 얻은 조각을 계층적 구조로 구성하여 후속 처리 및 렌더링을 용이하게 하는 것입니다. 수학적으로 이는 트리 다이어그램을 구축하는 것과 유사하며, 각 노드는 구문 단위를 나타내고 노드 간의 관계는 코드의 논리적 구조를 반영합니다.
구현
토큰화를 완료한 후 각 조각을 반복하고 구문 트리를 구축합니다. Node
클래스를 트리 노드의 기본 클래스로 사용하고 각 노드 유형에 대한 하위 클래스를 만듭니다. 각 하위 클래스는 process_fragment
및 render
메서드를 제공해야 합니다. process_fragment
는 조각 콘텐츠를 추가로 구문 분석하고 필요한 속성을 Node
객체에 저장하는 데 사용됩니다. render
메서드는 제공된 컨텍스트를 사용하여 해당 노드의 콘텐츠를 HTML로 변환하는 역할을 합니다.
다음은 Node
기본 클래스의 정의입니다.
class TemplateNode(object): def __init__(self, fragment=None): # 자식 노드 저장 self.children = [] # 새 범위 생성 여부 표시 self.creates_scope = False # 조각 처리 self.process_fragment(fragment) def process_fragment(self, fragment): pass def enter_scope(self): pass def render(self, context): pass def exit_scope(self): pass def render_children(self, context, children=None): if children is None: children = self.children def render_child(child): child_html = child.render(context) return '' if not child_html else str(child_html) return ''.join(map(render_child, children))
다음은 변수 노드의 정의입니다.
class TemplateVariable(_Node): def process_fragment(self, fragment): # 변수 이름 저장 self.name = fragment def render(self, context): # 컨텍스트에서 변수 값 확인 return resolve_in_context(self.name, context)
Node
유형을 결정하고 올바른 클래스를 초기화하려면 조각의 유형과 텍스트를 확인해야 합니다. 텍스트 및 변수 조각은 텍스트 노드와 변수 노드로 직접 변환할 수 있지만 블록 조각은 추가 처리가 필요하며 해당 유형은 블록 명령에 따라 결정됩니다. 예를 들어 {% each items %}
는 each
유형의 블록 노드입니다.
노드는 범위를 만들 수도 있습니다. 컴파일 중에 현재 범위를 기록하고 새 노드를 현재 범위의 자식 노드로 만듭니다. 올바른 닫는 태그가 발견되면 현재 범위가 닫히고 범위 스택에서 범위가 팝되어 스택의 맨 위가 새 범위가 됩니다. 예제 코드는 다음과 같습니다.
def template_compile(self): # 루트 노드 생성 root = _Root() # 범위 스택 초기화 scope_stack = [root] for fragment in self.each_fragment(): if not scope_stack: raise TemplateError('nesting issues') # 현재 범위 가져오기 parent_scope = scope_stack[-1] if fragment.type == CLOSE_BLOCK_FRAGMENT: # 현재 범위 종료 parent_scope.exit_scope() # 현재 범위 팝 scope_stack.pop() continue # 새 노드 생성 new_node = self.create_node(fragment) if new_node: # 새 노드를 현재 범위의 자식 노드 목록에 추가 parent_scope.children.append(new_node) if new_node.creates_scope: # 새 노드를 범위 스택에 추가 scope_stack.append(new_node) # 새 범위 진입 new_node.enter_scope() return root
3단계: 렌더링
원리
렌더링은 구성된 AST를 최종 HTML 출력으로 변환하는 프로세스입니다. 이 과정에서 AST 노드 유형 및 컨텍스트 정보에 따라 템플릿의 변수와 논리를 실제 값과 내용으로 대체해야 합니다. 수학적으로 이는 트리 구조를 순회하고 평가하여 규칙에 따라 각 노드의 정보를 변환하고 결합하는 것과 유사합니다.
구현
마지막 단계는 AST를 HTML로 렌더링하는 것입니다. 이 단계에서는 AST의 모든 노드를 방문하고 템플릿에 전달된 context
매개변수를 사용하여 render
메서드를 호출합니다. 렌더링 과정에서 render
는 컨텍스트 변수의 값을 지속적으로 확인합니다. ast.literal_eval
함수를 사용하여 Python 코드가 포함된 문자열을 안전하게 실행할 수 있습니다. 예제 코드는 다음과 같습니다.
import ast def eval_expression(expr): try: return 'literal', ast.literal_eval(expr) except (ValueError, SyntaxError): return 'name', expr
컨텍스트 변수가 리터럴 대신 사용되는 경우 컨텍스트에서 해당 값을 검색해야 합니다. 여기서는 점이 포함된 변수 이름과 두 개의 점을 사용하여 외부 컨텍스트에 액세스하는 변수를 처리해야 합니다. 다음은 resolve
함수 구현입니다.
def resolve(name, context): if name.startswith('..'): # 외부 컨텍스트 가져오기 context = context.get('..', {}) name = name[2:] try: for tok in name.split('.'): # 컨텍스트에서 변수 찾기 context = context[tok] return context except KeyError: raise TemplateContextError(name)
결론
이 간단한 예를 통해 템플릿 엔진의 작동 원리에 대한 기본적인 이해를 가질 수 있기를 바랍니다. 이 코드는 아직 프로덕션 수준과는 거리가 멀지만 보다 완벽한 도구를 개발하는 데 기초가 될 수 있습니다.
참조: https://github.com/alexmic/microtemplates
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로 Python 서비스를 배포하는 데 가장 적합한 플랫폼인 **Leapcell**을 추천합니다.
🚀 좋아하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 손쉽게 개발하십시오.
🌍 무료로 무제한 프로젝트 배포
사용한 만큼만 지불하십시오. 요청도 없고 요금도 없습니다.
⚡ 사용량에 따라 지불, 숨겨진 비용 없음
유휴 요금 없이 완벽한 확장성을 제공합니다.
🔹 Twitter에서 팔로우하세요: @LeapcellHQ