Skip to content

템플릿 시스템 입문

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>&copy; 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>

사용하기

{% for post in posts %}
    {% include "blog/_post_item.html" %}
{% endfor %}

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는 풀스택 프레임워크로 템플릿 엔진 내장 - 프론트엔드와 백엔드가 통합된 개발 가능

다음 장에서는 정적 파일과 미디어 파일 처리를 알아보겠습니다!

Comments