Skip to content

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의 인증 시스템을 알아보겠습니다!

Comments