[Django] Formsets: 여러 폼 동시 처리
Django formset, modelformset, inlineformset, management form, formset factory
정의
Formset = 여러 폼을 한 페이지에서 동시 처리. “상품 3개 한 번에 등록”, “주문 상세 여러 줄 편집” 같은 시나리오.
3가지 종류
flowchart TD
Q{Formset 종류}
Q --> A["FormSet<br/>(일반 Form 반복)"]
Q --> B["ModelFormSet<br/>(같은 모델 여러 개)"]
Q --> C["InlineFormSet<br/>(부모 모델의 자식들)"]
1. 기본 FormSet
from django import forms
from django.forms import formset_factory
class ContactForm(forms.Form):
name = forms.CharField()
email = forms.EmailField()
ContactFormSet = formset_factory(
ContactForm,
extra=2, # 빈 폼 개수
max_num=10, # 최대 개수
can_delete=True,
)
def contacts_view(request):
if request.method == 'POST':
formset = ContactFormSet(request.POST)
if formset.is_valid():
for form in formset:
if form.cleaned_data.get('DELETE'):
continue
name = form.cleaned_data['name']
email = form.cleaned_data['email']
# 저장 로직
else:
formset = ContactFormSet()
return render(request, 'contacts.html', {'formset': formset})
Template + Management Form
<form method="post">
{% csrf_token %}
{{ formset.management_form }} <!-- 필수! -->
{% for form in formset %}
<div class="form-row">
{{ form.as_p }}
</div>
{% endfor %}
<button type="submit">저장</button>
</form>
CAUTION
{{ formset.management_form }} 필수 - 폼 개수 등 메타데이터. 안 넣으면 ValidationError.
Management form 이 렌더링:
<input type="hidden" name="form-TOTAL_FORMS" value="2">
<input type="hidden" name="form-INITIAL_FORMS" value="0">
<input type="hidden" name="form-MIN_NUM_FORMS" value="0">
<input type="hidden" name="form-MAX_NUM_FORMS" value="10">
2. ModelFormSet
from django.forms import modelformset_factory
ProductFormSet = modelformset_factory(
Product,
fields=('name', 'price', 'stock'),
extra=1,
can_delete=True,
)
def products_admin(request):
if request.method == 'POST':
formset = ProductFormSet(request.POST, queryset=Product.objects.filter(active=True))
if formset.is_valid():
formset.save()
else:
formset = ProductFormSet(queryset=Product.objects.filter(active=True))
return render(request, 'products.html', {'formset': formset})
3. InlineFormSet (부모-자식)
from django.forms import inlineformset_factory
# Author 하나에 여러 Book
BookFormSet = inlineformset_factory(
Author, # 부모
Book, # 자식
fields=('title', 'published_at'),
extra=1,
can_delete=True,
)
def edit_author(request, author_id):
author = Author.objects.get(pk=author_id)
if request.method == 'POST':
formset = BookFormSet(request.POST, instance=author)
if formset.is_valid():
formset.save() # 자동으로 author 지정!
else:
formset = BookFormSet(instance=author)
return render(request, 'author_books.html', {'author': author, 'formset': formset})
동적 formset (JavaScript)
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
<div id="forms-container">
{% for form in formset %}
<div class="form-row">
{{ form.as_p }}
</div>
{% endfor %}
</div>
<button type="button" onclick="addForm()">폼 추가</button>
<button type="submit">저장</button>
</form>
<script>
function addForm() {
const container = document.getElementById('forms-container');
const totalForms = document.getElementById('id_form-TOTAL_FORMS');
const idx = parseInt(totalForms.value);
const template = container.children[0].outerHTML;
const newForm = template.replaceAll(`form-0-`, `form-${idx}-`);
container.insertAdjacentHTML('beforeend', newForm);
totalForms.value = idx + 1;
}
</script>
Validation
class BaseContactFormSet(forms.BaseFormSet):
def clean(self):
"""폼셋 전체 검증"""
if any(self.errors):
return
emails = []
for form in self.forms:
if form.cleaned_data.get('DELETE'):
continue
email = form.cleaned_data.get('email')
if email in emails:
raise forms.ValidationError('이메일 중복 금지')
emails.append(email)
ContactFormSet = formset_factory(
ContactForm,
formset=BaseContactFormSet,
extra=2,
)
Ordering (순서 변경)
ContactFormSet = formset_factory(
ContactForm,
extra=2,
can_order=True, # ← 순서 변경 UI
)
Django 6.0 개선
{{ formset.non_form_errors }}렌더링 개선- HTMX 통합 예시 문서 추가
- Type hints 강화
다른 프레임워크
| Framework | 여러 폼 처리 |
|---|---|
| Django | formset_factory, inlineformset_factory |
| Rails | accepts_nested_attributes_for + fields_for |
| Spring | List binding + @Valid |
| HTMX + Alpine | 동적 추가 |
흔한 함정
WARNING
- management form 누락 =
ValidationError. - DELETE 무시하고 저장 = 삭제 안 됨.
if form.cleaned_data.get('DELETE'): continue. - 동적 추가 시 form index 잘못 = server side 검증 실패.
- max_num 무시 = 무한 폼 추가 → DoS.
관련 위키
이 글의 용어 (3개)
- [Django] Form과 ModelFormdjango
- 정의 Django Form은 사용자 입력 정의·검증·렌더링·바인딩을 통합. 일반 은 임의 필드, 은 모델 자동 매핑. 검증과 보안(CSRF, SQL 인젝션 방지) 내장. Form…
- [Django] ModelForm: 모델 기반 폼 자동 생성django
- 정의 ModelForm = Model 로부터 Form 자동 생성. 필드 반복 정의 없이 CRUD 폼 완성. 일반 Form 은 참고. 왜 ModelForm? 기본 사용 vs [!W…
- [Django] View: 함수 기반 vs 클래스 기반django
- 정의 Django View는 HTTP 요청을 받아 응답을 반환하는 callable. 두 스타일이 공존. - FBV (Function-Based View): 일반 함수. 간단하고 …
💬 댓글