장고 프로젝트라면 마땅히 폼을 이용해야 할 것이다. 또한 대부분(95%)의 장고 프로젝트는 폼 중에서도 모델폼을 써야 할 것이다. 실제 모델폼은 91%의 장고 프로젝트에서 쓰이고 있다. 그 중 80%의 모델폼은 간단한 로직만으로 되어 있으며, 나머지 20%의 모델폼이 복잡한 로직을 담고 있다.
역할
form.cleaned_data
으로 제공
데이터를 변경하는 HTTP 폼은 언제타 CSRF 보안 (사이트 간 위조 요청 방지) 을 이용하자. 개발 단계에서 잊어버리고 이용하지 않았을 경우 보안 위험성이 있을 수 있다는 친절한 안내 메시지를 보여주기도 한다.
django-rest-framework 같은 API 프레임워크에서는 이러한 처리를 자동으로 다 해준다.
# forms.py
...
<form action="" method="post>
{% csrf_token %}
...
</form>
보통 Model 에 맞춰서 Form을 구성하기 때문에, Model이 바뀌면 Form도 그에 따라 의존되는 코드들을 변갱해 줘야하므로 유지보수가 귀찮다. 그러나 ModelForm을 사용하면 Model에서 사용할 Field만 정의하면 그 정보를 가져와 formField 를 자동으로 구성해주기 때문에 편하다.
특징
- Form : 직접 필드 정의, 위젯 설정이 필요
- ModelForm : 지정된 모델로부터 필드정보를 읽어들여, 자동으로 폼 필드를 세팅
# forms.py
from django import forms
# Form
class PostForm(forms.Form):
title = forms.CharField()
content = forms.CharField(widget=form.Textarea)
# ModelForm
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = '__all__' # 모든 필드를 불러온다.
모델폼 데이터는 폼에 먼저 저장된 이후 모델 인스턴스에 저장된다.
ModelForm에서 폼 데이터는 두 가지 각기 다른 단계를 통해 저장된다.
ModelForm 클래스에는 form.save(self, commit=True)
메서드가 구현되어 있는데, DB 저장 여부를 commit flag를 통해서 결정한다.
commit=False
를 사용하면 함수 호출을 지연시켜 DB에 바로 저장하지 않는다(임시 저장 상태).
DB에 실제 데이터를 저장하기 전에 따로 일련의 작업을 하고 싶을 때 사용하기 유용하다. form.save()
메서드에 의해 적용되기 전까지는 ModelForm이 모델 인스턴스로 저장되지 않는 분리된 특징 자체를 장점으로 이용할 수 있는 것이다.
# views.py
from django.shortcuts import render, redirect
from .models import Post
from .forms import PostForm
def post_new(request):
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
# 필수필드인 ip필드 값을 아직 채우지 않았으므로
# DB 세이브를 지연시켜 오류를 방지한다.
post = form.save(commit=False)
post.ip = request.META['REMOTE_ADDR'] # 유저로부터 ip 필드를 입력받음.
post.save()
return redirect('/dojo/')
else:
form = PostForm()
return render(request, 'dojo/post_form.html', {'form': form,})
form.is_valid()
를 통해서 검증을 통화한 값들은 dict 형태로 form.cleaned_data
변수명으로 제공된다.
⭐ 폼의 입력값을 얻고 싶은 경우는form.cleaned_data
로 접근하도록 하자.
➔ Avoid code : request.POST[]
로 접근
# views.py
...
form = CommentForm(request.POST)
if form.is_valid():
# request.POST 데이터는 폼 인스턴스 초기 데이터를 가져 오므로
# `form.clean()` 메서드를 통해 변경될 가능성이 있어
# 나쁜 접근이다.
message = request.POST['message']
comment = Comment(message=message)
comment.save()
return redirect(post)
➔ Good code : from.cleaned_data[]
로 접근
# views.py
...
form = CommentForm(request.POST)
if form.is_valid():
# 폼 인스턴스 내에서 clean함수를 통해 변환되었을 수도 있는 데이터를 가져오므로
# 좋은 접근이다.
message = form.cleaned_data['message']
comment = Comment(message=message)
comment.save()
return redirect(post)
장고 폼은 파이썬 딕셔너리의 유효성을 검사하는 최상의 도구이다.
유효성 검사를 수행하는데는 2가지의 방법이 있다.
1. validator
함수를 통한 유효성 검사
2. Form 클래스 내의 clean
, clean_
멤버함수를 통한 유효성 검사 및 값 변경
유효성 검사는 form.is_valid()
가 호출되는 시점에서 수행된다.
보통 from.is_valid()
가 호출될 때 다음과 같은 절차가 진행된다.
# views.py
def post_new(request):
if request.method == 'POST':
form = PostForm(request.POST, request:FILES)
if form.is_valid(): # 유효성 검사 수행
form.save()
# SUCESS 후 처리
else:
form = PostForm()
# ...
- 폼이 데이터를 받으면
form.is_valid()
는form.full_clean()
메서드를 호출한다.form.full_clean()
은 폼 필드들과 각각의 필드 유효성을 하나하나 검사하면서 다음과 같은 유효성 검사를 수행한다.
ㅤ- 필드에 들어온 데이터에 대해 파이썬 형식으로 변환하거나, 변환할 때 문제가 생기면 ValidationError를 일으킨다.
ㅤ- 커스텀 유효성 검사기(validator)를 포함한 각 필드에 특별한 유효성을 검사한다. 문제가 있을 때 ValidationError를 일으킨다.
ㅤ- 폼에clean_()
메서드가 있으면 이를 실행한다.
form.full_clean()
이form.clean()
메서드를 실행한다.- ModelForm 인스턴스의 경우
form.post_clean()
이 다음 작업을 한다.
ㅤ-form.is_valid()
가 True나 False로 설정되어 있는 것과 관계없이 ModelForm의 데이터를 모델 인스턴스로 설정한다.
ㅤ- 모델의clean()
메서드를 호출한다.
(1) forms.py
에 적용
›› form 에서는 리턴값을 따로 처리하지 않고, forms.ValidationError
예외 발생 유무로 처리
# forms.py
import re
from django import forms
from django.forms import ValidationError
def phone_number_validator(value):
if not re.match(r'^010[1-9]\d{7}$'):
raise forms.ValidationError('3글자 이상 입력하시오.')
class ProfileForm(forms.Form):
phone_number = forms.CharField(validators=[phone_number_validator])
(2) models.py
에 적용
›› model 클래스 정의 시에 validators
옵션 적용. (권장!! 로직이 분산되지 않는다.)
›› 가급적이면 모든 validators
는 모델에 정의하고, ModelForm 을 통해 모델의 validators
정보를 가져오도록 하자.
# models.py
import re
from django import forms
from django import models
def phone_number_validator(value):
if not re.match(r'^010[1-9]\d{7}$'):
raise forms.ValidationError('3글자 이상 입력하시오.')
class Profile(forms.Model):
phone_number = forms.CharField(validators=[phone_number_validator])
# forms.py
from django import forms
from .models import Profile
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = '__all__'