웹 애플리케이션을 개발할 때 동적 콘텐츠를 생성하는 것은 필수적입니다. Python 웹 프레임워크인 Flask에서는 Jinja2 템플릿 엔진을 사용하여 이를 쉽게 구현할 수 있습니다. 이 글에서는 Jinja2의 기본 개념부터 고급 기능까지 상세히 알아보겠습니다.
Jinja2란 무엇인가?
Jinja2는 Python으로 작성된 템플릿 엔진으로, Django의 템플릿 시스템에서 영감을 받아 개발되었습니다. Flask 프레임워크에 기본으로 내장되어 있으며, HTML 파일 내에 Python 코드와 유사한 문법을 사용하여 동적 콘텐츠를 생성할 수 있게 해줍니다.
Flask와 Jinja2 설정하기
Flask 애플리케이션에서 Jinja2를 사용하기 위해 먼저 Flask를 설치해야 합니다:
pip install flask
기본적인 Flask 애플리케이션 구조는 다음과 같습니다:
/my_flask_app
/static
/css
/js
/images
/templates
layout.html
index.html
about.html
app.py
app.py에서 Flask 애플리케이션을 설정하고 템플릿을 렌더링하는 기본 코드입니다:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', title='홈페이지', content='Jinja2 템플릿 엔진 예제입니다.')
if __name__ == '__main__':
app.run(debug=True)
Jinja2 기본 문법
Jinja2의 문법은 크게 세 가지 유형의 구문으로 나눌 수 있습니다:
- {{ … }} – 표현식: 변수나 표현식의 결과를 출력
- {% … %} – 문장: 제어 구조(if, for 등)를 실행
- {# … #} – 주석: 템플릿에서만 보이고 출력되지 않음
변수 사용하기
Flask에서 템플릿으로 변수를 전달하고 표시하는 방법입니다:
# app.py
@app.route('/user/')
def show_user(username):
user = {
'name': username,
'age': 25,
'is_admin': False
}
return render_template('user.html', user=user)
# user.html
<h1>사용자 정보</h1>
<p>이름: {{ user.name }}</p>
<p>나이: {{ user.age }}</p>
{% if user.is_admin %}
<p>관리자 권한이 있습니다.</p>
{% else %}
<p>일반 사용자입니다.</p>
{% endif %}
제어 구조
Jinja2는 다양한 제어 구조를 제공합니다:
조건문 (if-elif-else)
{% if user.age < 18 %}
<p>미성년자입니다.</p>
{% elif user.age < 65 %}
<p>성인입니다.</p>
{% else %}
<p>노인입니다.</p>
{% endif %}
반복문 (for)
# app.py
@app.route('/items')
def show_items():
items = ['사과', '바나나', '오렌지', '포도']
return render_template('items.html', items=items)
#item.html
<h1>상품 목록</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% if items|length > 0 %}
<p>총 {{ items|length }}개의 상품이 있습니다.</p>
{% else %}
<p>상품이 없습니다.</p>
{% endif %}
반복문 특수 변수
반복문 내에서 사용할 수 있는 특수 변수들이 있습니다:
<ul>
{% for item in items %}
<li class="{% if loop.first %}first{% elif loop.last %}last{% endif %}">
{{ loop.index }}. {{ item }}
</li>
{% endfor %}
</ul>
주요 loop 변수:
- loop.index: 1부터 시작하는 현재 반복 횟수
- loop.index0: 0부터 시작하는 현재 반복 횟수
- loop.first: 첫 번째 반복인 경우 True
- loop.last: 마지막 반복인 경우 True
- loop.length: 전체 항목 수
필터 사용하기
Jinja2의 필터는 변수를 변형하는 데 사용됩니다. 파이프(|) 기호를 사용하여 적용합니다:
{{ name|capitalize }}
{{ list|join(', ') }}
{{ text|truncate(100) }}
{{ date|dateformat('%Y-%m-%d') }}
자주 사용되는 필터:
- safe: HTML을 이스케이프하지 않고 그대로 출력
- capitalize: 첫 글자를 대문자로 변환
- lower/upper: 소문자/대문자로 변환
- trim: 앞뒤 공백 제거
- striptags: HTML 태그 제거
- join: 리스트를 문자열로 결합
- default: 값이 없을 때 기본값 제공
- length: 문자열이나 리스트의 길이 반환
<p>이름: {{ name|default('이름 없음')|upper }}</p>
<p>소개: {{ description|truncate(50, true, '...') }}</p>
템플릿 상속
템플릿 상속은 Jinja2의 가장 강력한 기능 중 하나입니다. 기본 레이아웃을 정의하고 자식 템플릿에서 특정 부분만 변경할 수 있습니다.
기본 레이아웃 템플릿 만들기
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}기본 제목{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<nav>
<ul>
<li><a href="{{ url_for('index') }}">홈</a></li>
<li><a href="{{ url_for('about') }}">소개</a></li>
<li><a href="{{ url_for('contact') }}">연락처</a></li>
</ul>
</nav>
</header>
<main>
{% block content %}
<p>기본 콘텐츠입니다.</p>
{% endblock %}
</main>
<footer>
<p>© 2023 내 웹사이트</p>
</footer>
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
자식 템플릿에서 상속받기
{% extends "layout.html" %}
{% block title %}홈페이지 - 내 웹사이트{% endblock %}
{% block content %}
<h1>환영합니다!</h1>
<p>이 웹사이트는 Jinja2 템플릿 엔진을 사용하여 만들어졌습니다.</p>
{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/home.css') }}">
{% endblock %}
매크로 사용하기
매크로는 재사용 가능한 템플릿 코드 조각으로, 함수처럼 사용할 수 있습니다:
{% macro input(name, value='', type='text', label='') %}
<div class="form-group">
{% if label %}
<label for="{{ name }}">{{ label }}</label>
{% endif %}
<input type="{{ type }}" name="{{ name }}" id="{{ name }}" value="{{ value }}">
</div>
{% endmacro %}
{% macro submit(value="제출") %}
<button type="submit" class="btn btn-primary">{{ value }}</button>
{% endmacro %}
다른 템플릿에서 매크로 사용하기:
{% extends "layout.html" %}
{% import "macros.html" as forms %}
{% block title %}연락처 - 내 웹사이트{% endblock %}
{% block content %}
<h1>연락하기</h1>
<form method="post">
{{ forms.input('name', label='이름') }}
{{ forms.input('email', type='email', label='이메일') }}
{{ forms.input('subject', label='제목') }}
<div class="form-group">
<label for="message">메시지</label>
<textarea name="message" id="message" rows="5"></textarea>
</div>
{{ forms.submit('메시지 보내기') }}
</form>
{% endblock %}
템플릿 포함하기 (include)
다른 템플릿 파일을 현재 템플릿에 포함시킬 수 있습니다:
<div class="sidebar">
<h3>최근 게시물</h3>
<ul>
{% for post in recent_posts %}
<li><a href="{{ url_for('post', id=post.id) }}">{{ post.title }}</a></li>
{% endfor %}
</ul>
</div>
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<h1>{{ post.title }}</h1>
<p class="meta">작성일: {{ post.date|dateformat('%Y-%m-%d') }}</p>
<div class="content">
{{ post.content|safe }}
</div>
</div>
<div class="col-md-4">
{% include 'sidebar.html' %}
</div>
</div>
</div>
{% endblock %}
Jinja2 고급 기능
사용자 정의 필터 만들기
Flask 애플리케이션에서 사용자 정의 필터를 추가할 수 있습니다:
# app.py
from datetime import datetime
@app.template_filter('dateformat')
def dateformat_filter(date, format='%Y-%m-%d'):
if isinstance(date, str):
date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S')
return date.strftime(format)
@app.template_filter('currency')
def currency_filter(value):
return f"{int(value):,}원"
템플릿에서 사용:
<p>가격: {{ product.price|currency }}</p>
<p>등록일: {{ product.created_at|dateformat('%Y년 %m월 %d일') }}</p>
전역 변수 설정
모든 템플릿에서 사용할 수 있는 전역 변수를 설정할 수 있습니다:
# app.py
@app.context_processor
def inject_globals():
return {
'site_name': '내 웹사이트',
'current_year': datetime.now().year,
'admin_email': 'admin@example.com'
}
템플릿에서 사용:
<footer>
<p>© {{ current_year }} {{ site_name }} | 문의: {{ admin_email }}</p>
</footer>
Jinja2 보안 고려사항
Jinja2는 기본적으로 모든 출력을 HTML 이스케이프하여 XSS(Cross-Site Scripting) 공격을 방지합니다. 하지만 |safe
필터를 사용하면 이스케이프를 비활성화할 수 있으므로 주의해야 합니다.
{{ user_input|safe }}
{{ user_input }}
실전 예제: 블로그 애플리케이션
간단한 블로그 애플리케이션을 만들어 Jinja2의 다양한 기능을 활용해 보겠습니다:
# app.py
from flask import Flask, render_template, request, redirect, url_for
from datetime import datetime
app = Flask(__name__)
# 임시 데이터베이스 역할
posts = [
{
'id': 1,
'title': 'Flask와 Jinja2 시작하기',
'content': '이 글에서는 Flask와 Jinja2의 기본 사용법을 알아봅니다.',
'author': '홍길동',
'created_at': datetime(2023, 1, 15)
},
{
'id': 2,
'title': 'Jinja2 템플릿 상속 활용하기',
'content': '템플릿 상속을 사용하면 코드 중복을 줄일 수 있습니다.',
'author': '김철수',
'created_at': datetime(2023, 2, 20)
}
]
@app.template_filter('dateformat')
def dateformat_filter(date, format='%Y-%m-%d'):
return date.strftime(format)
@app.context_processor
def inject_globals():
return {
'site_name': 'Flask 블로그',
'current_year': datetime.now().year
}
@app.route('/')
def index():
return render_template('blog/index.html', posts=posts)
@app.route('/post/')
def post(post_id):
post = next((p for p in posts if p['id'] == post_id), None)
if post is None:
return render_template('404.html'), 404
return render_template('blog/post.html', post=post, recent_posts=posts[:3])
if __name__ == '__main__':
app.run(debug=True)
블로그 레이아웃 템플릿:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{{ site_name }}{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<h1>{{ site_name }}</h1>
<nav>
<ul>
<li><a href="{{ url_for('index') }}">홈</a></li>
</ul>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© {{ current_year }} {{ site_name }}</p>
</footer>
</body>
</html>
블로그 인덱스 페이지:
{% extends "blog/layout.html" %}
{% block title %}{{ site_name }} - 최신 글{% endblock %}
{% block content %}
<h2>최신 글</h2>
{% if posts|length > 0 %}
<div class="posts">
{% for post in posts %}
<article class="post-preview">
<h3><a href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a></h3>
<p class="meta">
작성자: {{ post.author }} |
작성일: {{ post.created_at|dateformat('%Y년 %m월 %d일') }}
</p>
<p class="excerpt">{{ post.content|truncate(100) }}</p>
<a href="{{ url_for('post', post_id=post.id) }}" class="read-more">더 읽기</a>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
</div>
{% else %}
<p>아직 작성된 글이 없습니다.</p>
{% endif %}
{% endblock %}
블로그 포스트 상세 페이지:
{% extends "blog/layout.html" %}
{% block title %}{{ post.title }} - {{ site_name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<article class="post">
<h2>{{ post.title }}</h2>
<p class="meta">
작성자: {{ post.author }} |
작성일: {{ post.created_at|dateformat('%Y년 %m월 %d일') }}
</p>
<div class="content">
{{ post.content }}
</div>
</article>
<div class="navigation">
<a href="{{ url_for('index') }}">< 목록으로 돌아가기</a>
</div>
</div>
<div class="col-md-4">
<div class="sidebar">
<h3>최근 게시물</h3>
<ul>
{% for recent in recent_posts %}
<li>
<a href="{{ url_for('post', post_id=recent.id) }}">
{{ recent.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
결론
Jinja2는 Flask 애플리케이션에서 HTML 템플릿을 쉽게 작성할 수 있게 해주는 강력한 템플릿 엔진입니다. 변수 사용, 제어 구조, 필터, 템플릿 상속 등의 기능을 통해 코드 중복을 줄이고 유지보수가 쉬운 웹 애플리케이션을 개발할 수 있습니다.
이 글에서 다룬 내용을 기반으로 직접 Flask와 Jinja2를 활용한 웹 애플리케이션을 개발해 보시기 바랍니다. 실제 프로젝트에 적용하면서 더 많은 경험을 쌓을 수 있을 것입니다.
답글 남기기