목차8개 항목
사용자 입력을 HTML <input>으로 직접 받아 검증하는 일은 번거롭고 실수하기 쉽다. Django Form은 이 작업의 상당 부분을 자동화하고, 직접 짜는 것보다 안전하다.
Form이 해 주는 일
- 렌더링을 위한 데이터 준비·재구성
- 데이터에 맞는 HTML form 생성
- 클라이언트가 제출한 데이터 수신·처리, 그리고 무엇보다 유효성 검사
Form vs ModelForm
| Form | ModelForm | |
|---|---|---|
| 필드 정의 | 직접 정의 | 지정한 Model의 필드로 자동 생성 |
| 저장 | 직접 Model.objects.create(...) | form.save()로 바로 저장 |
| 쓰임 | 검색창·로그인처럼 DB 저장과 무관한 입력 | 모델 데이터를 생성·수정하는 입력 |
어느 쪽이 더 낫다기보다 용도에 따라 고른다.
# forms.py: Form: 필드를 직접 정의 (사용자가 입력할 필드만)
from django import forms
class RecordForm(forms.Form):
date = forms.CharField(max_length=8)
push_up = forms.CharField(max_length=20)
pull_up = forms.CharField(widget=forms.Textarea)# forms.py: ModelForm: Meta에 모델과 필드만 지정
from .models import Record
class RecordForm(forms.ModelForm):
class Meta:
model = Record
fields = '__all__' # 또는 ('date', 'push_up') / exclude = ('created_at',)템플릿 렌더링
<form action="{% url 'records:create' %}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<button>저장</button>
</form>| 옵션 | 렌더링 |
|---|---|
{{ form.as_p }} | 각 필드를 <p>로 감쌈 |
{{ form.as_ul }} | 각 필드를 <li>로 (<ul>은 직접 작성) |
{{ form.as_table }} | 각 필드를 <tr>로 (<table>은 직접 작성) |
{{ form.as_div }} | 각 필드를 <div>로 (Django 4.1+) |
생성 view 패턴
GET이면 빈 폼을 보여 주고, POST면 검증 후 저장하는 하나의 view로 new/create를 합친다.
from django.shortcuts import render, redirect
def create(request):
if request.method == 'POST': # (A) 제출
form = RecordForm(request.POST) # 입력값이 채워진(bound) 폼
if form.is_valid(): # (B) 유효성 검사
record = form.save()
return redirect('records:detail', record.pk) # (C) 성공
else: # (D) 처음 접근(GET)
form = RecordForm() # 빈 폼
return render(request, 'records/create.html', {'form': form}) # (E)context/render를else밖에 둔 이유: POST인데 검증에 실패한 경우에도 (E)로 내려와 폼을 다시 보여 주기 위해서다.- 이때 다시 보여 주는 폼은 빈 폼이 아니라 사용자가 입력한 값과 에러 메시지를 담은 폼이다. 사용자는 잘못된 부분만 고쳐 다시 제출하면 된다.
- 수정(update)도 같은 구조에서
RecordForm(request.POST, instance=record)/RecordForm(instance=record)만 바꾸면 된다.
흔한 실수: GET 요청에서도
RecordForm(request.POST)를 만들면 처음 페이지에 들어갈 때부터 "필수 항목입니다" 에러가 표시된다. GET에서는 인자 없는 빈 폼을 만든다.
작성자 채우기: commit=False
작성자(user)나 부모 글(movie) 같은 외래키는 사용자가 입력하는 값이 아니므로 폼 필드에서 빼고, view에서 채워 넣는다. save(commit=False)는 DB에 저장하기 전의 인스턴스만 돌려준다.
if form.is_valid():
comment = form.save(commit=False) # 아직 DB에 저장하지 않음
comment.movie = movie
comment.user = request.user
comment.save()이미지 업로드
| 단계 | 내용 |
|---|---|
| 모델 | ImageField (Pillow 필요) |
| 설정 | MEDIA_ROOT(저장 경로), MEDIA_URL(접근 URL 접두사) |
| 템플릿 | <form>에 enctype="multipart/form-data" |
| view | MovieForm(request.POST, request.FILES)로 파일까지 전달 |
| 개발 서버 | 프로젝트 urls.py에 + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) |
{% if movie.image %}
<img src="{{ movie.image.url }}" alt="{{ movie.title }}">
{% endif %}enctype이나 request.FILES를 빠뜨리면 오류 없이 파일만 저장되지 않으므로 업로드가 안 될 때 가장 먼저 확인한다.
Widget
위젯은 input 요소의 HTML 렌더링만 담당한다. 유효성 검사는 필드가 담당한다는 점을 혼동하지 않는다.
class RecordForm(forms.ModelForm):
# 방법 1: 필드를 다시 선언하며 위젯 지정
pull_up = forms.CharField(
label='턱걸이',
widget=forms.TextInput(attrs={'class': 'pull_up', 'placeholder': '턱걸이 개수'}),
)
class Meta:
model = Record
fields = '__all__'
# 방법 2: Meta.widgets에서 지정
widgets = {
'push_up': forms.TextInput(attrs={'class': 'push_up', 'placeholder': '팔굽혀펴기 개수'}),
}방법 1은 라벨·검증 옵션까지 함께 바꿀 수 있고, 방법 2는 모델 필드 정의를 그대로 둔 채 렌더링만 바꿀 때 간결하다.
핵심 정리
- Form은 렌더링 + 검증, ModelForm은 여기에 모델 필드 재사용 +
save()까지. - view는
if POST → is_valid → save → redirect / else → 빈 폼 → render패턴. - 검증 실패 시 입력값·에러가 담긴 폼을 그대로 다시 렌더링한다.
관련 노트: 웹 보안: CSRF와 XSS: {% csrf_token %}이 막는 공격, Django URL과 View: render와 redirect
참고: Django Forms 문서