템플릿 시스템 입문
FastAPI는 주로 JSON 응답을 반환하지만, Django는 HTML 템플릿을 렌더링하여 완전한 웹 페이지를 생성합니다. 이번 장에서는 Django 템플릿 시스템을 알아보겠습니다.
1. 템플릿이란?
템플릿은 동적 데이터를 포함할 수 있는 HTML 파일입니다. FastAPI에서는 프론트엔드가 별도로 존재하지만, Django는 서버에서 HTML을 생성합니다.
FastAPI vs Django 접근법
FastAPI (API + 프론트엔드 분리):
# FastAPI: JSON 응답
@app.get("/posts")
async def get_posts():
return {"posts": [{"id": 1, "title": "Hello"}]}
# 프론트엔드 (React/Vue)에서 별도 처리
Django (서버 사이드 렌더링):
# Django: HTML 렌더링
def post_list(request):
posts = Post.objects.all()
return render(request, 'blog/post_list.html', {'posts': posts})
2. 템플릿 설정
템플릿 디렉토리 구조
mysite/
├── blog/
│ └── templates/
│ └── blog/ # 앱 이름으로 네임스페이스
│ ├── base.html
│ ├── post_list.html
│ └── post_detail.html
├── templates/ # 프로젝트 레벨 템플릿
│ ├── base.html
│ └── home.html
└── mysite/
└── settings.py
settings.py 설정
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # 프로젝트 레벨 템플릿
'APP_DIRS': True, # 앱 내부 templates 폴더 자동 탐색
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
3. Django 템플릿 언어 (DTL)
변수 출력
<!-- 변수 -->
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<!-- 속성 접근 -->
<p>작성자: {{ post.author.username }}</p>
<p>작성일: {{ post.created_at }}</p>
<!-- 메서드 호출 (인자 없는 메서드만) -->
<p>댓글 수: {{ post.get_comment_count }}</p>
필터
<!-- 대문자 변환 -->
{{ post.title|upper }}
<!-- 기본값 설정 -->
{{ post.description|default:"설명이 없습니다." }}
<!-- 날짜 포맷 -->
{{ post.created_at|date:"Y-m-d H:i" }}
<!-- 문자열 자르기 -->
{{ post.content|truncatewords:30 }}
<!-- 여러 필터 체이닝 -->
{{ post.title|lower|capfirst }}
태그
<!-- if 문 -->
{% if user.is_authenticated %}
<p>안녕하세요, {{ user.username }}님!</p>
{% else %}
<p>로그인해주세요.</p>
{% endif %}
<!-- for 문 -->
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.content|truncatewords:50 }}</p>
</article>
{% empty %}
<p>아직 포스트가 없습니다.</p>
{% endfor %}
<!-- URL 태그 -->
<a href="{% url 'blog:post_detail' pk=post.pk %}">자세히 보기</a>
<!-- CSRF 토큰 (폼 필수) -->
<form method="post">
{% csrf_token %}
<input type="text" name="title">
<button type="submit">제출</button>
</form>
4. 템플릿 상속
기본 템플릿 (templates/base.html)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>{% block title %}My Site{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{% url 'home' %}">홈</a>
<a href="{% url 'blog:post_list' %}">블로그</a>
{% if user.is_authenticated %}
<a href="{% url 'logout' %}">로그아웃</a>
{% else %}
<a href="{% url 'login' %}">로그인</a>
{% endif %}
</nav>
</header>
<main>
{% block content %}
{% endblock %}
</main>
<footer>
<p>© 2024 My Site</p>
</footer>
{% block extra_js %}{% endblock %}
</body>
</html>
상속받은 템플릿 (blog/templates/blog/post_list.html)
{% extends "base.html" %}
{% block title %}블로그 - {{ block.super }}{% endblock %}
{% block content %}
<h1>블로그 포스트</h1>
<div class="post-list">
{% for post in posts %}
<article class="post">
<h2>
<a href="{% url 'blog:post_detail' pk=post.pk %}">
{{ post.title }}
</a>
</h2>
<p class="meta">
작성자: {{ post.author.username }} |
{{ post.created_at|date:"Y-m-d" }}
</p>
<p>{{ post.content|truncatewords:50 }}</p>
</article>
{% endfor %}
</div>
<!-- 페이지네이션 -->
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">이전</a>
{% endif %}
<span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">다음</a>
{% endif %}
</div>
{% endblock %}
5. 템플릿 Include
재사용 가능한 템플릿 조각을 만들 수 있습니다.
_post_item.html
<article class="post-item">
<h3>{{ post.title }}</h3>
<p>{{ post.content|truncatewords:20 }}</p>
<a href="{% url 'blog:post_detail' pk=post.pk %}">더 읽기</a>
</article>
사용하기
6. 정적 파일과 함께 사용
{% load static %}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<img src="{% static 'images/logo.png' %}" alt="Logo">
<!-- 미디어 파일 -->
{% if post.image %}
<img src="{{ post.image.url }}" alt="{{ post.title }}">
{% endif %}
</body>
</html>
7. 커스텀 템플릿 태그와 필터
커스텀 필터 생성 (blog/templatetags/blog_extras.py)
from django import template
import markdown
register = template.Library()
@register.filter
def markdown_format(text):
"""마크다운을 HTML로 변환"""
return markdown.markdown(text)
@register.filter
def add_class(field, css_class):
"""폼 필드에 CSS 클래스 추가"""
return field.as_widget(attrs={"class": css_class})
사용하기
{% load blog_extras %}
{{ post.content|markdown_format|safe }}
{{ form.title|add_class:"form-control" }}
8. 컨텍스트 프로세서
전역적으로 사용할 변수를 템플릿에 제공합니다.
커스텀 컨텍스트 프로세서
# blog/context_processors.py
def site_info(request):
return {
'site_name': 'My Blog',
'site_description': 'Django로 만든 블로그',
'current_year': datetime.now().year,
}
# settings.py
TEMPLATES = [
{
'OPTIONS': {
'context_processors': [
# ... 기본 프로세서들
'blog.context_processors.site_info',
],
},
},
]
9. 실습: 블로그 템플릿 만들기
1. 기본 레이아웃 (templates/base.html)
{% load static %}
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Django Blog{% endblock %}</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
header { background: #333; color: white; padding: 1rem; margin: -20px -20px 20px; }
nav a { color: white; text-decoration: none; margin-right: 1rem; }
.container { max-width: 800px; margin: 0 auto; }
.post { border-bottom: 1px solid #eee; padding: 1rem 0; }
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<div class="container">
<h1>Django Blog</h1>
<nav>
<a href="{% url 'blog:post_list' %}">홈</a>
{% if user.is_authenticated %}
<a href="{% url 'blog:post_create' %}">글쓰기</a>
<span>{{ user.username }}</span>
{% endif %}
</nav>
</div>
</header>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
2. 포스트 목록 (blog/templates/blog/post_list.html)
{% extends "base.html" %}
{% block title %}포스트 목록 - {{ block.super }}{% endblock %}
{% block content %}
<h2>최신 포스트</h2>
{% for post in posts %}
<div class="post">
<h3><a href="{% url 'blog:post_detail' pk=post.pk %}">{{ post.title }}</a></h3>
<p class="meta">{{ post.author }} - {{ post.created_at|date:"Y년 m월 d일" }}</p>
<p>{{ post.content|truncatewords:30 }}</p>
</div>
{% empty %}
<p>아직 포스트가 없습니다.</p>
{% endfor %}
{% endblock %}
3. 포스트 상세 (blog/templates/blog/post_detail.html)
{% extends "base.html" %}
{% block title %}{{ post.title }} - {{ block.super }}{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<p class="meta">
작성자: {{ post.author.username }} |
작성일: {{ post.created_at|date:"Y-m-d H:i" }}
{% if post.updated_at %}
| 수정일: {{ post.updated_at|date:"Y-m-d H:i" }}
{% endif %}
</p>
<div class="content">
{{ post.content|linebreaks }}
</div>
{% if user == post.author %}
<div class="actions">
<a href="{% url 'blog:post_edit' pk=post.pk %}">수정</a>
<a href="{% url 'blog:post_delete' pk=post.pk %}">삭제</a>
</div>
{% endif %}
</article>
<a href="{% url 'blog:post_list' %}">목록으로</a>
{% endblock %}
10. 템플릿 디버깅
Debug 모드에서 변수 확인
<!-- 모든 변수 출력 -->
<pre>{{ debug }}</pre>
<!-- 특정 변수 타입 확인 -->
{{ posts|pprint }}
<!-- 조건부 디버깅 -->
{% if settings.DEBUG %}
<div class="debug">
<h3>Debug Info</h3>
<p>User: {{ user }}</p>
<p>Posts count: {{ posts|length }}</p>
</div>
{% endif %}
정리
Django 템플릿 시스템의 특징: - 서버 사이드 렌더링: 서버에서 HTML 생성 - 템플릿 상속: DRY 원칙으로 코드 재사용 - 풍부한 내장 필터와 태그: 다양한 데이터 변환 - 안전한 출력: 자동 HTML 이스케이프 - 확장 가능: 커스텀 태그와 필터 작성
FastAPI와의 차이: - FastAPI는 주로 JSON 응답, Django는 HTML 렌더링 - Django는 풀스택 프레임워크로 템플릿 엔진 내장 - 프론트엔드와 백엔드가 통합된 개발 가능
다음 장에서는 정적 파일과 미디어 파일 처리를 알아보겠습니다!