Django Form Field 검증 커스터마이징
Django Form의 필드 검증 로직을 커스터마이징하는 다양한 방법을 알아봅니다.
Django Form Field 검증 커스터마이징
Django Form은 데이터 검증을 자동으로 처리하지만, 특정 비즈니스 로직에 맞는 검증이 필요할 때가 많습니다. 이 글에서는 다양한 검증 커스터마이징 방법을 알아봅니다.
Environment
$ python --version
Python 3.11.5
$ pip show django
Name: Django
Version: 4.2.5Problem: 기본 검증만으로 부족한 상황
from django import forms
class RegistrationForm(forms.Form):
username = forms.CharField(max_length=50)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
# Basic validation only checks field types
form = RegistrationForm(data={
'username': 'joel',
'email': 'joel@example.com',
'password': 'mypassword',
'confirm_password': 'differentpassword'
})
print(form.is_valid()) # True - but passwords don't match!Analysis: Django 검증 프로세스
# Django form validation flow:
# 1. Field-level validation (field.clean)
# 2. Form-level validation (form.clean)
# 3. Model validation (if ModelForm)
# Each field has its own clean method
class CharField(forms.Field):
def clean(self, value):
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return valueSolution: 다양한 검증 커스터마이징 방법
방법 1: clean_ 메서드
from django import forms
import re
class RegistrationForm(forms.Form):
username = forms.CharField(max_length=50)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
def clean_username(self):
username = self.cleaned_data['username']
# Check if username contains only alphanumeric characters
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise forms.ValidationError(
"Username can only contain letters, numbers, and underscores."
)
# Check length
if len(username) < 3:
raise forms.ValidationError(
"Username must be at least 3 characters long."
)
return username.lower()
def clean_email(self):
email = self.cleaned_data['email']
# Check if email is from a specific domain
blocked_domains = ['tempmail.com', 'throwaway.com']
domain = email.split('@')[1]
if domain in blocked_domains:
raise forms.ValidationError(
f"Emails from {domain} are not allowed."
)
return email
def clean_password(self):
password = self.cleaned_data['password']
# Custom password validation
if len(password) < 8:
raise forms.ValidationError(
"Password must be at least 8 characters long."
)
if not re.search(r'[A-Z]', password):
raise forms.ValidationError(
"Password must contain at least one uppercase letter."
)
if not re.search(r'[0-9]', password):
raise forms.ValidationError(
"Password must contain at least one digit."
)
return password방법 2: clean() 메서드로 폼 레벨 검증
class RegistrationForm(forms.Form):
username = forms.CharField(max_length=50)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm_password = cleaned_data.get('confirm_password')
if password and confirm_password:
if password != confirm_password:
raise forms.ValidationError(
"Passwords do not match."
)
# Cross-field validation
username = cleaned_data.get('username')
if username and password:
if username in password:
raise forms.ValidationError(
"Password cannot contain your username."
)
return cleaned_data방법 3: 커스텀 필드 클래스
from django import forms
from django.core.exceptions import ValidationError
class PhoneNumberField(forms.CharField):
def __init__(self, *args, **kwargs):
self.country_code = kwargs.pop('country_code', '+82')
super().__init__(*args, **kwargs)
def clean(self, value):
value = super().clean(value)
# Remove spaces and dashes
value = re.sub(r'[\s\-]', '', value)
# Add country code if not present
if not value.startswith('+'):
value = self.country_code + value
# Validate format
if not re.match(r'^\+\d{10,15}$', value):
raise ValidationError(
"Invalid phone number format."
)
return value
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
phone = PhoneNumberField(country_code='+351')방법 4: 커스텀 validator 함수
from django.core.exceptions import ValidationError
def validate_even(value):
if value % 2 != 0:
raise ValidationError(f'{value} is not an even number.')
def validate_file_size(max_size_mb=5):
def validator(file):
max_size = max_size_mb * 1024 * 1024
if file.size > max_size:
raise ValidationError(
f'File size must be less than {max_size_mb}MB.'
)
return validator
class UploadForm(forms.Form):
number = forms.IntegerField(validators=[validate_even])
document = forms.FileField(
validators=[validate_file_size(max_size_mb=10)]
)방법 5: ModelForm 검증
from django.db import models
from django import forms
class Product(models.Model):
name = forms.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField(default=0)
class Meta:
model = Product
fields = ['name', 'price', 'stock']
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ['name', 'price', 'stock']
def clean_stock(self):
stock = self.cleaned_data['stock']
if stock < 0:
raise forms.ValidationError("Stock cannot be negative.")
return stock
def clean(self):
cleaned_data = super().clean()
price = cleaned_data.get('price')
stock = cleaned_data.get('stock')
if price and stock and price < 1 and stock > 100:
raise forms.ValidationError(
"Low-price items cannot have high stock."
)
return cleaned_dataLessons Learned
clean_
: 개별 필드의 검증이 필요하면 clean_<fieldname>메서드를 오버라이드하세요.clean() 메서드: 여러 필드 간의 교차 검증이 필요하면
clean()메서드를 사용하세요.커스텀 필드: 재사용 가능한 검증 로직은 커스텀 필드 클래스로 만드는 것이 좋습니다.
validator 함수: 간단한 검증 로직은 validator 함수로 분리하면 테스트가 쉬워집니다.
에러 메시지: 사용자 친화적인 에러 메시지를 작성하여 어떤 문제가 있는지 명확하게 알려주세요.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.