Jinja2 — 표의 한 줄이 화면의 한 줄이 되는 곳
풀스택 뼈대 ③ 화면을 만드는 층
템플릿은 빈칸이 있는 상장 양식입니다. 이름만 바꿔 100장을 찍어 내죠.
학교에서 상장을 100장 만든다고 생각해 보세요.
100장을 처음부터 손으로 쓸까요? 아니죠. 양식 한 장을 만들고
이름과 상 이름만 바꿔 가며 인쇄합니다.
Jinja2(진자투)는 Flask에 기본으로 들어 있는 템플릿 엔진입니다.
설치할 것도, 배울 문법도 거의 없어요. 딱 세 가지만 알면 됩니다.
| 문법 | 하는 일 | 예시 |
{{ 값 }} | 값을 그 자리에 출력 | {{ p.name }} → 텀블러 |
{% for %} | 여러 줄을 반복 | 상품 6개 → 카드 6개 |
{% if %} | 조건에 따라 보이거나 감추기 | 재고 0이면 '품절' |
기억하세요. 중괄호 두 개는 출력, 중괄호-퍼센트는 명령입니다.
DB → 화면, 전체 그림 한 장
왼쪽이 2층(Flask), 오른쪽이 3층(Jinja2)입니다. 두 파일이 짝을 이룹니다.
# ── app.py (2층) ─────────────────────────
@app.route("/")
def product_list():
conn = db()
products = conn.execute(
"SELECT id, name, price, stock FROM product ORDER BY id").fetchall()
conn.close()
return render_template("products.html", products=products)
# ↑ 파일 이름 ↑ 템플릿에 넘길 데이터
<!-- ── templates/products.html (3층) ─────── -->
<h1>상품 목록 ({{ products|length }}개)</h1>
<table>
<tr><th>이름</th><th>가격</th><th>재고</th></tr>
{% for p in products %} <!-- DB의 행 하나 = 화면의 줄 하나 -->
<tr>
<td>{{ p.name }}</td>
<td>{{ "{:,}".format(p.price) }}원</td>
<td>
{% if p.stock > 0 %}
{{ p.stock }}개 남음
{% else %}
<b>품절</b>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
템플릿 파일은 반드시 templates/ 폴더 안에 두어야 합니다.
Flask가 그곳을 자동으로 찾아보거든요.
🧪 템플릿 엔진을 직접 만들어 보기
Jinja2가 마법처럼 보이나요? 사실 원리는 아주 단순합니다.
빈칸을 찾아 값으로 바꿔치기하는 것뿐이에요. 직접 만들어 봅시다.
템플릿 = 양식 + 데이터. 상품을 하나 더 추가해 보세요 — 화면이 저절로 늘어납니다.
🎮 Jinja2 실습실 — 직접 써 보고 바로 확인하세요
아래 편집기에 템플릿을 쓰면 이 서버의 진짜 Jinja2가 렌더링해 돌려줍니다.
왼쪽 아래에 보이는 products 데이터는 여러분의 연습 DB에서 실제로 읽어 온 행이에요.
타이핑을 멈추면 자동으로 다시 그려집니다.
- ② 반복 예제를 열고
{% for %} 줄을 지워 보세요. 화면이 어떻게 바뀌나요?
- 생성된 HTML 탭을 눌러 보세요. 템플릿 6줄이 몇 줄의 HTML이 되었나요?
- ⑤ Bootstrap 카드 — 3층과 4층이 만나는 그 코드가 실제로 어떻게 보이는지 확인하세요.
- ⑥ 자동 이스케이프 —
{{ evil }} 에 |safe 를 붙였다 떼며 비교해 보세요.
- ⑦ 오류 —
{% endfor %} 를 빠뜨리면 Jinja2가 몇 번째 줄이라고 알려 주는지 보세요.
상속 — 같은 껍데기를 100번 쓰지 않기
사이트의 모든 페이지에는 같은 것이 들어갑니다. 상단 메뉴, 로고, 하단 정보…
페이지마다 복사하면 메뉴 하나 바꿀 때 모든 파일을 고쳐야 하죠.
10강의 중복의 저주가 화면에서도 똑같이 일어나는 겁니다.
<!-- templates/base.html — 모든 페이지의 공통 껍데기 -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>{% block title %}우리 쇼핑몰{% endblock %}</title>
</head>
<body>
<nav><a href="{{ url_for('product_list') }}">🛍️ 우리 쇼핑몰</a></nav>
{% block content %}{% endblock %} <!-- 여기에 각 페이지 내용이 들어온다 -->
<footer>(주)디앤에이보이 · 2026</footer>
</body>
</html>
<!-- templates/products.html — 껍데기를 물려받는다 -->
{% extends "base.html" %}
{% block title %}상품 목록{% endblock %}
{% block content %}
<h1>오늘의 상품</h1>
{% for p in products %}
<p>{{ p.name }} — {{ p.price }}원</p>
{% endfor %}
{% endblock %}
이제 메뉴를 바꾸려면 base.html 한 파일만 고치면 됩니다.
표를 나눠 중복을 없앤 정규화와 똑같은 발상이에요.
url_for('product_list')는 주소를 직접 쓰는 대신
함수 이름으로 주소를 만들어 달라는 뜻입니다. 나중에 주소가 바뀌어도 링크가 깨지지 않아요.
🤔 확인 문제
상품이 6개에서 60개로 늘었습니다. 무엇을 고쳐야 할까요?
해설 템플릿은 한 줄의 모양만 정의합니다. 몇 줄을 그릴지는 DB가 돌려준 행의 수가 결정해요. 데이터가 늘어도 코드는 그대로 — 이것이 템플릿 엔진의 힘입니다.