Django Forms 기초
이전 장에서는 수동으로 폼을 처리했지만, Django는 강력한 Forms 시스템을 제공합니다. FastAPI의 Pydantic과 유사하게 데이터 검증과 HTML 폼 생성을 자동화할 수 있습니다.
1. Django Forms vs Pydantic
개념 비교
Pydantic (FastAPI):
from pydantic import BaseModel, validator
class PostCreate(BaseModel):
title: str
content: str
@validator('title')
def title_min_length(cls, v):
if len(v) < 5:
raise ValueError('제목은 5자 이상이어야 합니다')
return v
Django Forms:
from django import forms
class PostForm(forms.Form):
title = forms.CharField(min_length=5, max_length=200)
content = forms.CharField(widget=forms.Textarea)
2. Form 클래스 기본
기본 Form 생성
# blog/forms.py
from django import forms
class PostForm(forms.Form):
title = forms.CharField(
max_length=200,
min_length=5,
label='제목',
help_text='5자 이상 200자 이하로 입력하세요.'
)
content = forms.CharField(
widget=forms.Textarea(attrs={'rows': 10, 'cols': 80}),
label='내용',
required=True
)
category = forms.ChoiceField(
choices=[
('tech', '기술'),
('life', '일상'),
('travel', '여행'),
],
initial='tech',
label='카테고리'
)
is_published = forms.BooleanField(
required=False,
initial=False,
label='즉시 공개'
)
View에서 Form 사용
from django.shortcuts import render, redirect
from .forms import PostForm
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
# 검증된 데이터 접근
title = form.cleaned_data['title']
content = form.cleaned_data['content']
category = form.cleaned_data['category']
# 데이터 처리
post = Post.objects.create(
title=title,
content=content,
category=category,
author=request.user
)
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
3. Form 필드 타입
from django import forms
from django.core.validators import MinValueValidator, MaxValueValidator
class ExampleForm(forms.Form):
# 텍스트 필드
char_field = forms.CharField(max_length=100)
email_field = forms.EmailField()
url_field = forms.URLField(required=False)
slug_field = forms.SlugField()
# 텍스트 영역
text_field = forms.CharField(widget=forms.Textarea)
# 숫자 필드
integer_field = forms.IntegerField(
validators=[MinValueValidator(1), MaxValueValidator(100)]
)
float_field = forms.FloatField()
decimal_field = forms.DecimalField(max_digits=5, decimal_places=2)
# 날짜/시간
date_field = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
time_field = forms.TimeField(widget=forms.TimeInput(attrs={'type': 'time'}))
datetime_field = forms.DateTimeField()
# 선택 필드
choice_field = forms.ChoiceField(choices=[('a', 'A'), ('b', 'B')])
multiple_choice = forms.MultipleChoiceField(
choices=[('a', 'A'), ('b', 'B'), ('c', 'C')],
widget=forms.CheckboxSelectMultiple
)
# 파일 필드
file_field = forms.FileField(required=False)
image_field = forms.ImageField(required=False)
# 불린 필드
boolean_field = forms.BooleanField(required=False)
4. ModelForm - 모델과 연동
ModelForm 생성
from django import forms
from .models import Post
class PostModelForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'tags']
# 또는 모든 필드: fields = '__all__'
# 또는 제외: exclude = ['author', 'created_at']
labels = {
'title': '제목',
'content': '내용',
}
help_texts = {
'title': '포스트 제목을 입력하세요.',
}
widgets = {
'content': forms.Textarea(attrs={'rows': 10}),
'tags': forms.CheckboxSelectMultiple(),
}
ModelForm으로 CRUD 구현
def post_create(request):
if request.method == 'POST':
form = PostModelForm(request.POST)
if form.is_valid():
# commit=False로 저장 전 수정 가능
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # ManyToMany 필드 저장
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostModelForm()
return render(request, 'blog/post_form.html', {'form': form})
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == 'POST':
form = PostModelForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('blog:post_detail', pk=post.pk)
else:
form = PostModelForm(instance=post)
return render(request, 'blog/post_form.html', {'form': form})
5. 템플릿에서 Form 렌더링
자동 렌더링
<!-- 가장 간단한 방법 -->
<form method="post">
{% csrf_token %}
{{ form }}
<button type="submit">제출</button>
</form>
<!-- 테이블 형태 -->
<form method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<button type="submit">제출</button>
</form>
<!-- 단락 형태 -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">제출</button>
</form>
<!-- 리스트 형태 -->
<form method="post">
{% csrf_token %}
{{ form.as_ul }}
<button type="submit">제출</button>
</form>
수동 렌더링 (커스터마이징)
<form method="post" novalidate>
{% csrf_token %}
<!-- 에러 표시 -->
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<!-- 개별 필드 -->
<div class="form-group">
{{ form.title.label_tag }}
{{ form.title }}
{% if form.title.help_text %}
<small class="form-text text-muted">{{ form.title.help_text }}</small>
{% endif %}
{% if form.title.errors %}
<div class="invalid-feedback d-block">
{{ form.title.errors }}
</div>
{% endif %}
</div>
<!-- Bootstrap 스타일 적용 -->
<div class="form-group">
<label for="{{ form.content.id_for_label }}">{{ form.content.label }}</label>
{{ form.content|add_class:"form-control" }}
{% if form.content.errors %}
{% for error in form.content.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<button type="submit" class="btn btn-primary">제출</button>
</form>
6. Form 유효성 검사
필드별 검증
from django import forms
from django.core.exceptions import ValidationError
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category']
def clean_title(self):
title = self.cleaned_data.get('title')
if '광고' in title:
raise ValidationError('제목에 "광고"를 포함할 수 없습니다.')
# 중복 체크
qs = Post.objects.filter(title=title)
if self.instance.pk: # 수정인 경우
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError('이미 존재하는 제목입니다.')
return title
def clean_content(self):
content = self.cleaned_data.get('content')
if len(content) < 10:
raise ValidationError('내용은 10자 이상이어야 합니다.')
return content
전체 폼 검증
class PostForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput, required=False)
class Meta:
model = Post
fields = ['title', 'content', 'is_private']
def clean(self):
cleaned_data = super().clean()
is_private = cleaned_data.get('is_private')
password = cleaned_data.get('password')
if is_private and not password:
raise ValidationError('비공개 포스트는 비밀번호가 필요합니다.')
return cleaned_data
7. 위젯 커스터마이징
from django import forms
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'tags', 'published_date']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '제목을 입력하세요'
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 10,
'placeholder': '내용을 입력하세요'
}),
'category': forms.Select(attrs={
'class': 'form-select'
}),
'tags': forms.SelectMultiple(attrs={
'class': 'form-select',
'size': 5
}),
'published_date': forms.DateTimeInput(attrs={
'type': 'datetime-local',
'class': 'form-control'
}),
}
8. 파일 업로드 처리
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'thumbnail', 'attachment']
def clean_thumbnail(self):
thumbnail = self.cleaned_data.get('thumbnail')
if thumbnail:
if thumbnail.size > 5 * 1024 * 1024: # 5MB
raise ValidationError('이미지 크기는 5MB를 초과할 수 없습니다.')
# 이미지 형식 체크
import imghdr
if imghdr.what(thumbnail) not in ['jpeg', 'png', 'gif']:
raise ValidationError('JPEG, PNG, GIF 형식만 허용됩니다.')
return thumbnail
# View
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST, request.FILES) # FILES 추가
if form.is_valid():
form.save()
return redirect('blog:post_list')
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
9. Formset - 여러 폼 처리
from django.forms import formset_factory, modelformset_factory
# 일반 Formset
CommentFormSet = formset_factory(CommentForm, extra=3, max_num=10)
# Model Formset
CommentModelFormSet = modelformset_factory(
Comment,
fields=['content', 'is_approved'],
extra=1,
can_delete=True
)
# View에서 사용
def manage_comments(request, post_pk):
post = get_object_or_404(Post, pk=post_pk)
if request.method == 'POST':
formset = CommentModelFormSet(
request.POST,
queryset=Comment.objects.filter(post=post)
)
if formset.is_valid():
comments = formset.save(commit=False)
for comment in comments:
comment.post = post
comment.save()
return redirect('blog:post_detail', pk=post_pk)
else:
formset = CommentModelFormSet(
queryset=Comment.objects.filter(post=post)
)
return render(request, 'blog/manage_comments.html', {
'formset': formset,
'post': post
})
10. 실습: 회원가입 폼
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class SignUpForm(UserCreationForm):
email = forms.EmailField(
required=True,
help_text='유효한 이메일 주소를 입력하세요.'
)
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise ValidationError('이미 사용중인 이메일입니다.')
return email
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
if commit:
user.save()
return user
# views.py
def signup(request):
if request.method == 'POST':
form = SignUpForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
messages.success(request, '회원가입이 완료되었습니다!')
return redirect('home')
else:
form = SignUpForm()
return render(request, 'registration/signup.html', {'form': form})
커스텀 템플릿 태그 (위젯 스타일링)
# templatetags/form_tags.py
from django import template
register = template.Library()
@register.filter
def add_class(field, css_class):
return field.as_widget(attrs={'class': css_class})
@register.filter
def add_attr(field, attr_string):
"""Usage: {{ form.field|add_attr:"placeholder:Enter text" }}"""
attrs = {}
for attr in attr_string.split(','):
key, value = attr.split(':')
attrs[key.strip()] = value.strip()
return field.as_widget(attrs=attrs)
정리
Django Forms의 장점: - 자동 HTML 생성: 폼 필드를 HTML로 자동 변환 - 강력한 유효성 검사: 필드별, 폼 전체 검증 - 보안 기능 내장: CSRF, XSS 보호 - ModelForm: 모델과 긴밀한 통합 - 재사용성: 폼 클래스를 여러 뷰에서 재사용
FastAPI/Pydantic과 비교: - Django Forms는 HTML 렌더링까지 처리 - Pydantic은 주로 데이터 검증에 집중 - Django는 위젯으로 UI 커스터마이징 가능 - 두 프레임워크 모두 강력한 유효성 검사 제공
다음 장에서는 Django의 인증 시스템을 알아보겠습니다!