파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 강의를 듣고 정리한 글입니다.
django-bootstrap4 라이브러리를 활용함으로써 간단한 템플릿 문법만으로 부트스트랩 폼이나 기타 엘리먼트를 랜더링 할 수 있다.
또한 템플릿 엔진의 상속 기능을 이용해서 템플릿 관리가 가능하다. 특히나 form 템플릿을 한번만 정의해두고 여러 종류의 폼이나 모델 폼을 랜더링 할 수 있는것이 인상적이었다.
본 포스팅에서는 회원가입 폼을 만들어보면서 기본 프로젝트 레이아웃을 구성해본다.
accounts앱을 생성하고 부트스트랩을 포함한 후의 프로젝트 구조이다.
# 기본 프로젝트 구조 (디렉토리만 출력)
askcompany # project_root
├── accounts
│ ├── migrations
│ └── templates
│ └── accounts
└── askcompany
├── settings
├── static
│ └── bootstrap-4.6.1-dist
│ ├── css
│ └── js
└── templates
$ 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,
})
구현한 accounts앱을 settings에 추가해준다.
INSTALLED_APPS = [
# ...
'accounts',
]
커스텀 유저 모델을 마이그레이션
$ python manage.py makemigrations accounts # 프로젝트 초기에 마이그레이션이 필요하다.
$ python manage.py migrate
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 디렉토리로 옮긴다.
이 라이브러리는 장고 템플릿 문법으로 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 코드를 작성하게 되는 구조이다.
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
템플릿 로더가 참조하는 경로를 추가해줘야 한다.
(askcompany/ascompany/templates에 프로젝트 레벨의 템플릿 구현)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ # 장고 템플릿 로더가 참조하는 경로들
BASE_DIR / 'askcompany' / 'templates'
],
'APP_DIRS': True, # 장고 템플릿 로더가 앱 내의 템플릿 경로도 참조할지 정의한다.
# ...
}
]
모든 베이스가 되는 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>
프로젝트가 커질 수록 다양한 장고 앱에서 회원가입 폼, 포스트 폼 등 다양한 폼을 구현하게 될 것이다. 이 때 이 폼을 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을 extends하고 끝낸다.
{% extends 'layout.html' %}
: 템플릿 로더의 경로로부터 layout.html을 찾아서 extends한다. 위에서 구현한 layout.html이다.
askcompany/accounts/templates/accounts/layout.html
{% extends 'layout.html' %}
{% 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 %}