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 FormDRF Serializer를 각각 어떤 식으로 활용할 수 있는지, 그리고 어느 쪽이 더 자연스럽고 실용적인 선택인지를 정리해 보겠습니다.

HTMX 요청 처리에서 Form과 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,
    })

여기서 포인트는 아주 분명합니다.

  1. HTMX가 보낸 값을 request.POST로 받는다
  2. Form이 검증한다
  3. 성공하면 HTML partial을 반환한다
  4. 실패하면 에러가 담긴 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 를 활용하는 감각" 도 함께 회복할 필요가 있다고 생각합니다.


관련글읽기