템플릿 상속과 django-bootstrap4를 이용한 기본 레이아웃 구성

guava·2021년 12월 29일
0

파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 강의를 듣고 정리한 글입니다.

django-bootstrap4 라이브러리를 활용함으로써 간단한 템플릿 문법만으로 부트스트랩 폼이나 기타 엘리먼트를 랜더링 할 수 있다.

또한 템플릿 엔진의 상속 기능을 이용해서 템플릿 관리가 가능하다. 특히나 form 템플릿을 한번만 정의해두고 여러 종류의 폼이나 모델 폼을 랜더링 할 수 있는것이 인상적이었다.

본 포스팅에서는 회원가입 폼을 만들어보면서 기본 프로젝트 레이아웃을 구성해본다.

최종 프로젝트 구조

accounts앱을 생성하고 부트스트랩을 포함한 후의 프로젝트 구조이다.

# 기본 프로젝트 구조 (디렉토리만 출력)
askcompany  # project_root
├── accounts
│   ├── migrations
│   └── templates
│       └── accounts
└── askcompany
    ├── settings
    ├── static
    │   └── bootstrap-4.6.1-dist
    │       ├── css
    │       └── js
    └── templates

인증 관리를 위한 accounts앱 구현

accounts앱 추가

$ django-admin startapp accounts

커스텀 유저 모델, 회원가입 폼, 뷰 구현

모델

# askcompany/accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    website_url = models.URLField(blank=True)
    bio = models.TextField(blank=True)

# askcompany/accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm

from .models import User


class SignupForm(UserCreationForm):  # 장고에서 제공하는 UserCreationForm를 상속받아 활용
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['email'].required = True
        self.fields['first_name'].required = True
        self.fields['last_name'].required = True

    class Meta(UserCreationForm.Meta):  # Meta 클래스를 덮어 써버리지 않기위해 상속받아서 구현한다.
        model = User  # User객체를 현재 프로젝트에서 사용하는 User객체로 해야한다. 아니면 기본 auth.User객체를 사용하기 때문.
        fields = ['username', 'email', 'first_name', 'last_name']

    def clean_email(self):
        email = self.cleaned_data.get('email')
        if email:
            qs = User.objects.filter(email=email)
            if qs.exists():
                raise forms.ValidationError('이미 등록된 이메일 주소입니다.')
        return email

# askcompany/accounts/views.py
from django.contrib import messages
from django.shortcuts import render, redirect

from accounts.forms import SignupForm


def signup(request):
    if request.method == 'POST':
        form = SignupForm(request.POST)
        if form.is_valid():
            user = form.save()
            messages.success(request, '회원가입 환영합니다.')
            next_url = request.GET.get('next', '/')
            return redirect(next_url)
    else:
        form = SignupForm()
    return render(request, 'accounts/signup_form.html', {
        'form': form,
    })

settings.py의 INSTALLED_APPS에 포함

구현한 accounts앱을 settings에 추가해준다.

INSTALLED_APPS = [
    # ...
    'accounts',
]

마이그레이션

커스텀 유저 모델을 마이그레이션

$ python manage.py makemigrations accounts  # 프로젝트 초기에 마이그레이션이 필요하다.
$ python manage.py migrate

bootstrap4를 장고 프로젝트에 포함

https://getbootstrap.com/docs/4.6/getting-started/download/

$ wget https://github.com/twbs/bootstrap/releases/download/v4.6.1/bootstrap-4.6.1-dist.zip
$ unzip bootstrap-4.6.1-dist.zip  # 압축해제 후 askcompany/askcompany/static 디렉토리로 옮긴다.

django-bootstrap4 설치

이 라이브러리는 장고 템플릿 문법으로 bootstrap4를 사용할 수 있게 해준다.

https://django-bootstrap4.readthedocs.io/en/latest/installation.html

poetry add django-bootstrap4

settings.py의 INSTALLED_APPS에 추가

INSTALLED_APPS = [
    # ...
    'bootstrap4',
]

레이아웃 구조 잡기

프로젝트 레벨의 layout.html을 작성하고 나머지 앱 레벨에서도 각각 layout.html을 작성한다. 앱 레벨의 layout.html은 프로젝트 레벨의 layout.html을 extends한다.

앱의 나머지 템플릿들은 각 앱의 layout.html을 extends해서 필요한 코드를 추가하게 된다.

즉 상위 layout.html에 모든 베이스가되는 html 코드를 작성하고 하위의 템플릿에서는 각 페이지에 필요한 html 코드를 작성하게 되는 구조이다.

최종 레이아웃 위치 (layout.html)

askcompany  # project_root
├── accounts
│   ├── migrations
│   └── templates
│       └── accounts
│           ├── layout.html  # askcompany/askcompany/templates/layout.html을 extends한다.
│           └── signup_form.html  # askcompany/accounts/templates/accounts를 extends하며 askcompany/askcompany/templates/_form.html를 include한다.
└── askcompany
    ├── settings
    ├── static
    │   └── bootstrap-4.6.1-dist
    │       ├── css
    │       └── js
    └── templates
        ├── _form.html
        ├── layout.html  # 루트 layout.html
        └── root.html

settings.py의 템플릿 로더 설정 참고

템플릿 로더가 참조하는 경로를 추가해줘야 한다.
(askcompany/ascompany/templates에 프로젝트 레벨의 템플릿 구현)

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [  # 장고 템플릿 로더가 참조하는 경로들
            BASE_DIR / 'askcompany' / 'templates'  
        ],
        'APP_DIRS': True,  # 장고 템플릿 로더가 앱 내의 템플릿 경로도 참조할지 정의한다.
        # ...
     }
 ]

프로젝트 레벨의 layout.html

모든 베이스가 되는 html 태그들은 layout.html에서 구현한다.

아래 세개의 템플릿 문법만 보면 된다. (html 코드들은 잠시 무시하고 보자.)
{% load static %}: static을 로드한다.
{% if messages %}~{% endif %}: messages 라이브러리를 이용해 메시지를 출력한다.
{% block content %}~{% endblock %}: extends한 템플릿에서 이 부분만 구현하면 된다.
askcompany/askcompany/templates/layout.html

{% load static %}
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>Instagram with Ask Company</title>
  <link rel="stylesheet" href="{% static 'bootstrap-4.6.1-dist/css/bootstrap.css' %}">
  <script src="{% static 'jquery-3.6.0.min.js' %}"></script>
  <script src="{% static 'bootstrap-4.6.1-dist/js/bootstrap.js' %}"></script>
</head>
<body>
{% if messages %}
  {% for message in messages %}
    <div class="alert alert-{{ message.tags }}">
      {{ message }}
    </div>
  {% endfor %}
{% endif %}

<div class="border-bottom mb-3">
  <div class="container">
    <div class="row">
      <div class="col-sm-12">
        <div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 bg-white">
          <h5 class="my-0 mr-md-auto font-weight-normal">
            <img src="{% static 'logo.png' %}" alt="Instagram" style="width: 103px;">
          </h5>
          <nav class="my-2 my-md-0 mr-md-3">
            <a class="p-2 text-dark" href="/explore/">
              <svg aria-label="사람 찾기" class="_8-yf5 " color="#262626" fill="#262626" height="24" role="img"
                   viewBox="0 0 24 24" width="24">
                <polygon fill="none" points="13.941 13.953 7.581 16.424 10.06 10.056 16.42 7.585 13.941 13.953"
                         stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
                         stroke-width="2"></polygon>
                <polygon fill-rule="evenodd" points="10.06 10.056 13.949 13.945 7.581 16.424 10.06 10.056"></polygon>
                <circle cx="12.001" cy="12.005" fill="none" r="10.5" stroke="currentColor" stroke-linecap="round"
                        stroke-linejoin="round" stroke-width="2"></circle>
              </svg>
            </a>
            <a class="p-2 text-dark" href="/accounts/activity/">
              <svg aria-label="활동 피드" class="_8-yf5 " color="#262626" fill="#262626" height="24" role="img"
                   viewBox="0 0 24 24" width="24">
                <path
                  d="M16.792 3.904A4.989 4.989 0 0121.5 9.122c0 3.072-2.652 4.959-5.197 7.222-2.512 2.243-3.865 3.469-4.303 3.752-.477-.309-2.143-1.823-4.303-3.752C5.141 14.072 2.5 12.167 2.5 9.122a4.989 4.989 0 014.708-5.218 4.21 4.21 0 013.675 1.941c.84 1.175.98 1.763 1.12 1.763s.278-.588 1.11-1.766a4.17 4.17 0 013.679-1.938m0-2a6.04 6.04 0 00-4.797 2.127 6.052 6.052 0 00-4.787-2.127A6.985 6.985 0 00.5 9.122c0 3.61 2.55 5.827 5.015 7.97.283.246.569.494.853.747l1.027.918a44.998 44.998 0 003.518 3.018 2 2 0 002.174 0 45.263 45.263 0 003.626-3.115l.922-.824c.293-.26.59-.519.885-.774 2.334-2.025 4.98-4.32 4.98-7.94a6.985 6.985 0 00-6.708-7.218z"></path>
              </svg>
            </a>
            <a class="p-2 text-dark" href="#">
              Profile
            </a>
          </nav>
        </div>
      </div>
    </div>
  </div>
</div>


{% block content %}

{% endblock %}
<div class="border-top">
  <div class="container">
    <footer class="pt-4 my-md-5 pt-md-5">
      <div class="row">
        <div class="col-12 col-md">
          <img class="mb-2" src="/docs/4.6/assets/brand/bootstrap-solid.svg" alt="" width="24" height="24">
          <small class="d-block mb-3 text-muted">© 2017-2021</small>
        </div>
        <div class="col-6 col-md">
          <h5>Features</h5>
          <ul class="list-unstyled text-small">
            <li><a class="text-muted" href="#">Cool stuff</a></li>
            <li><a class="text-muted" href="#">Random feature</a></li>
            <li><a class="text-muted" href="#">Team feature</a></li>
            <li><a class="text-muted" href="#">Stuff for developers</a></li>
            <li><a class="text-muted" href="#">Another one</a></li>
            <li><a class="text-muted" href="#">Last time</a></li>
          </ul>
        </div>
        <div class="col-6 col-md">
          <h5>Resources</h5>
          <ul class="list-unstyled text-small">
            <li><a class="text-muted" href="#">Resource</a></li>
            <li><a class="text-muted" href="#">Resource name</a></li>
            <li><a class="text-muted" href="#">Another resource</a></li>
            <li><a class="text-muted" href="#">Final resource</a></li>
          </ul>
        </div>
        <div class="col-6 col-md">
          <h5>About</h5>
          <ul class="list-unstyled text-small">
            <li><a class="text-muted" href="#">Team</a></li>
            <li><a class="text-muted" href="#">Locations</a></li>
            <li><a class="text-muted" href="#">Privacy</a></li>
            <li><a class="text-muted" href="#">Terms</a></li>
          </ul>
        </div>
      </div>
    </footer>
  </div>
</div>
</body>
</html>

프로젝트 레벨의 form

프로젝트가 커질 수록 다양한 장고 앱에서 회원가입 폼, 포스트 폼 등 다양한 폼을 구현하게 될 것이다. 이 때 이 폼을 include해서 사용한다.

{% load bootstrap4 %} : django-bootstrap4를 로드한다.
{% bootstrap_form form %}: django-bootstrap4 라이브러리에서 제공하는 폼 템플릿에 우리의 form을 삽입한다.
{% buttons %}~{% endbuttons %}: buttons을 구현한다.
{{ submit_label|default:"Submit" }}: 버튼 이름을 include 하는 측의 템플릿에서 입력받는다. 입력받은 것이 없다면 submit을 출력한다.

askcompany/askcompany/templates/_form.html

{% load bootstrap4 %}
<form action="" method="POST" enctype="multipart/form-data">
  {% csrf_token %}
  {% bootstrap_form form %}
  {% buttons %}
    <button type="submit" class="btn btn-primary">
      {{ submit_label|default:"Submit" }}
    </button>
  {% endbuttons %}
</form>

앱 레벨의 layout.html

위에서 구현한 프로젝트 레벨의 layout.html을 extends하고 끝낸다.

{% extends 'layout.html' %}: 템플릿 로더의 경로로부터 layout.html을 찾아서 extends한다. 위에서 구현한 layout.html이다.

askcompany/accounts/templates/accounts/layout.html

{% extends 'layout.html' %}

앱 레벨의 form

{% extends 'accounts/layout.html' %}: 앱 레벨의 layout.html을 extends한다.
{% block content %}~{% endblock %} : 이 안에 필요한 내용을 구현하면 된다.
{% include '_form.html' with submit_label='회원가입' %} : 템플릿 로더의 경로에 정의해 둔 _form.html을 include한다. 이 때, submit_label에 '회원가입'이라는 문자를 삽입해준다.

다른 폼을 작성할 때도 이 코드에서 거의 변경점이 없다.

예를 들어 포스팅 폼을 출력한다고 하자. 이 때 필드 및 위젯은 폼 필드에 포함되어 있고 나머지 배경 스타일은 레이아웃에 있기 때문에 submit_label만 별도의 이름으로 삽입해주면 된다. (submit_label는 버튼 이름이다.)

askcompany/accounts/templates/accounts/signup_form.html

{% extends 'accounts/layout.html' %}

{% block content %}
  <div class="container">
    <div class="row">
      <div class="col-sm-6 offset-sm-3">
        {% include '_form.html' with submit_label='회원가입' %}
      </div>
    </div>
  </div>
{% endblock %}

0개의 댓글