Django와 HTMX로 동적 웹 개발을 단순화하기 : Forms와 Serializer 의 활용
지난 글에서는 HTMX가 데이터를 서버로 전송하는 방식을 살펴보았습니다.
지난글보기 : Django와 HTMX로 동적 웹 개발 단순화하기 (4편): Payload 전송 방식
기존 자바스크립트의 fetch()가 JSON payload를 직접 만들어 보내는 방식에 가깝다면, HTMX는 DOM에서 값을 수집해 form-data처럼 보내는 방식에 더 가깝다는 점을 확인했죠.
그렇다면 이제 자연스럽게 이런 질문이 떠오릅니다.
"HTMX로 들어온 이 데이터를, Django에서는 무엇으로 검증하는 것이 가장 자연스러울까?"
처음에는 많은 분들이 DRF Serializer를 떠올릴지도 모릅니다. 실제로 Serializer는 강력한 유효성 검사 도구이고, JSON이 아니더라도 사용할 수 있습니다. 하지만 막상 HTMX와 함께 써보면 어딘가 조금 억지스럽다는 느낌이 들기도 합니다.
왜일까요?
그 이유는 단순합니다.
HTMX의 기본 흐름은 HTML form의 세계에 더 가깝고, Django에는 바로 그 세계를 위해 설계된 Form이 이미 존재하기 때문입니다.
이번 글에서는 HTMX 요청 처리에서 Django Form과 DRF Serializer를 각각 어떤 식으로 활용할 수 있는지, 그리고 어느 쪽이 더 자연스럽고 실용적인 선택인지를 정리해 보겠습니다.

HTMX가 보내는 데이터는 결국 "폼 데이터"에 가깝다
지난 편에서 살펴본 것처럼, HTMX는 기본적으로 HTML 요소의 값을 수집해서 서버로 전송합니다. <form> 안의 입력값을 보내거나, hx-include로 특정 요소를 포함시키거나, hx-vals로 추가 값을 붙이는 식이죠.
즉, HTMX의 기본 철학은 다음에 가깝습니다.
- 자바스크립트 객체를 직접 조립하지 않는다
- HTML 요소에서 값을 모은다
- 서버에 요청을 보낸다
- JSON보다 HTML 조각 응답과 더 잘 어울린다
이 흐름은 생각보다 아주 중요합니다. 왜냐하면 이 구조는 전형적인 Django Form 처리 흐름과 거의 일치하기 때문입니다.
Django Form 역시 다음과 같은 전제를 가지고 설계되어 있습니다.
- 사용자가 HTML form에 입력한다
- 서버가
request.POST를 받는다 - Form이 데이터를 검증한다
- 에러가 있으면 다시 렌더링한다
- 문제가 없으면 저장하고 결과를 응답한다
이쯤 되면 HMX과 Django Form은 신데렐라와 유리구두마냥 딱 맞아 떨어지는 것 같은 기분이 들기 시작합니다.
따라서 이런 결론을 내려도 되지 않을까 싶습니다.
HTMX와 가장 자연스럽게 맞물리는 Django의 검증 도구는 DRF Serializer보다 Django Form이다.
왜 Django Form이 더 잘 어울릴까
DRF Serializer는 물론 훌륭한 도구입니다. 하지만 원래의 설계 맥락을 생각하면, Serializer는 데이터 직렬화와 API 입력 검증에 더 가까운 도구입니다.
반면 Django Form은 처음부터 다음을 위해 만들어졌습니다.
- HTML 폼 입력 처리
- 서버 사이드 유효성 검사
- 에러 메시지 표시
- 입력값 유지
- 템플릿 재렌더링
즉, HTMX처럼 "HTML 일부를 다시 받아서 바꿔치기하는 방식" 과 붙여 놓았을 때 훨씬 더 자연스럽습니다.
예를 들어 생각해 봅시다.
사용자가 댓글 작성 폼을 제출합니다.
- 유효성 검사에 실패하면?
- 입력했던 내용은 유지되어야 합니다
- 어떤 필드가 문제인지 보여줘야 합니다
- 에러가 붙은 폼을 다시 부분 렌더링해야 합니다
이런 UX는 Django Form이 정말 잘합니다.
물론 Serializer도 errors를 제공하고, 검증도 가능합니다. 하지만 그다음 단계인 "HTML 폼 UX 전체를 다시 구성하는 일" 은 Form 쪽이 훨씬 매끈합니다.
그래서 HTMX와의 궁합만 놓고 보면, Form이 기본 선택이라고 보는 것이 맞습니다.
가장 자연스러운 조합: HTMX + Django Form
먼저 가장 기본적인 예제를 보겠습니다. 간단한 할 일 등록 폼입니다.
Form 정의
from django import forms
class TodoForm(forms.Form):
title = forms.CharField(max_length=100, label="제목")
priority = forms.IntegerField(min_value=1, max_value=5, label="우선순위")
이제 뷰에서는 request.POST를 Form에 그대로 넣어 검증하면 됩니다.
View 처리
from django.shortcuts import render
from django.http import HttpResponse
def todo_create(request):
if request.method == "POST":
form = TodoForm(request.POST)
if form.is_valid():
title = form.cleaned_data["title"]
priority = form.cleaned_data["priority"]
# 저장 로직 수행
# Todo.objects.create(title=title, priority=priority)
return render(request, "todos/partials/todo_item.html", {
"title": title,
"priority": priority,
})
return render(request, "todos/partials/todo_form.html", {
"form": form,
}, status=400)
form = TodoForm()
return render(request, "todos/partials/todo_form.html", {
"form": form,
})
여기서 포인트는 아주 분명합니다.
- HTMX가 보낸 값을
request.POST로 받는다 - Form이 검증한다
- 성공하면 HTML partial을 반환한다
- 실패하면 에러가 담긴 Form을 다시 렌더링한다
이 흐름은 Django스럽고, HTMX스럽고, 무엇보다 유지보수가 편합니다.
템플릿에서 에러를 자연스럽게 다시 보여주기
Django Form이 특히 빛나는 부분은 바로 여기입니다.
검증 실패 시, Form 객체에는 이미 다음 정보가 들어 있습니다.
- 사용자가 입력했던 값
- 필드별 에러 메시지
- non-field 에러
- 어떤 필드가 유효하지 않은지에 대한 상태
따라서 partial 템플릿에서는 이를 그대로 렌더링하면 됩니다.
todo_form.html
<form hx-post="{% url 'todo_create' %}" hx-target="#todo-form-wrap" hx-swap="outerHTML">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="error-box">
{{ form.non_field_errors }}
</div>
{% endif %}
<div>
<label for="{{ form.title.id_for_label }}">제목</label>
{{ form.title }}
{% if form.title.errors %}
<div class="field-error">{{ form.title.errors }}</div>
{% endif %}
</div>
<div>
<label for="{{ form.priority.id_for_label }}">우선순위</label>
{{ form.priority }}
{% if form.priority.errors %}
<div class="field-error">{{ form.priority.errors }}</div>
{% endif %}
</div>
<button type="submit">등록</button>
</form>
이 예제는 매우 단순하지만, HTMX와 Django Form의 궁합을 잘 보여줍니다.
- 실패 시 폼 전체를 다시 렌더링할 수 있고
- 사용자가 입력한 값도 유지되며
- 에러 메시지도 필드 옆에 자연스럽게 붙습니다
기존의 fetch() + JSON + 수동 DOM 조작 방식이었다면, 이런 흐름을 구현하기 위해 자바스크립트를 꽤 많이 써야 했을 것입니다. 하지만 HTMX와 Django Form 조합에서는 서버 쪽 코드와 템플릿만으로 훨씬 단순하게 처리할 수 있습니다.
그러면 Serializer는 필요 없을까?
Serializer가 필요 없다. 라는 말은 너무 나간 것 같고, 이정도로 표현하면 좋지 않을까 싶습니다. 굳이 Serializer를 기본으로 선택할 것은 아니다 정도가 어떨까요?
실제로 DRF Serializer는 다음과 같은 상황에서 여전히 충분히 매력적입니다.
- 이미 프로젝트 전반에서 DRF를 적극 사용 중인 경우
- 같은 검증 로직을 API와 서버 렌더링 화면에서 함께 재사용하고 싶은 경우
- 입력 검증 로직이 복잡하고, 이를 Serializer에 이미 잘 정리해 둔 경우
- 나중에 동일한 기능을 모바일 앱이나 외부 API에도 노출할 계획이 있는 경우
즉, Serializer는 "HTMX에서 쓸 수 있느냐?" 의 문제가 아니라, "굳이 여기서까지 Serializer를 가져오는 게 전체 아키텍처상 이득이 있느냐?" 의 문제에 가까울 것입니다.
Serializer도 사용할 수는 있다
예를 들어, 이미 아래와 같은 Serializer가 있다고 해봅시다.
from rest_framework import serializers
class TodoSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
priority = serializers.IntegerField(min_value=1, max_value=5)
이 경우 HTMX 요청에서도 충분히 사용할 수 있습니다.
from django.shortcuts import render
def todo_create_with_serializer(request):
if request.method == "POST":
serializer = TodoSerializer(data=request.POST)
if serializer.is_valid():
title = serializer.validated_data["title"]
priority = serializer.validated_data["priority"]
return render(request, "todos/partials/todo_item.html", {
"title": title,
"priority": priority,
})
return render(request, "todos/partials/todo_form_serializer.html", {
"errors": serializer.errors,
"data": request.POST,
}, status=400)
보시는 것처럼 기술적으로는 전혀 문제 없습니다. Serializer는 JSON만 받는 도구가 아니기 때문입니다.
하지만 여기서 미묘한 차이가 드러납니다.
Form을 사용할 때는:
- 입력값 유지
- 필드별 렌더링
- 에러 바인딩
- 템플릿과의 연결
이 자연스럽게 이어집니다.
반면 Serializer를 사용할 때는:
serializer.errors를 직접 템플릿 구조에 맞게 풀어야 하고- 기존 입력값도 별도로 넘겨야 하며
- HTML form 재렌더링과의 연결을 개발자가 더 많이 신경 써야 합니다
즉, 사용할 수는 있지만, 조금 더 수작업이 늘어나는 편입니다.
바로 이 지점 때문에 HTMX와 함께 쓸 때 Serializer가 약간 억지스럽게 느껴질 수 있습니다.
마무리
지난 편에서 우리는 HTMX가 데이터를 어떻게 보내는지를 살펴보았습니다. 그리고 이번 편에서는 그 데이터를 Django에서 어떻게 검증하는 것이 가장 자연스러운지 정리해 보았습니다.
- HTMX는 기본적으로 form-data와 잘 어울립니다
- Django Form은 바로 그 흐름을 위해 설계된 도구입니다
- 그래서 HTMX와의 궁합만 놓고 보면 Form이 가장 자연스럽습니다
- DRF Serializer는 사용할 수는 있지만, 보다 전략적인 선택지에 가깝습니다
개인적으로는 HTMX를 제대로 활용하려면, "AJAX를 HTML스럽게 다루는 감각" 뿐 아니라 "Django Form과 form tags 를 활용하는 감각" 도 함께 회복할 필요가 있다고 생각합니다.
관련글읽기
댓글이 없습니다.