❣️ HTMX를 통해 JS없이 모던 웹 채팅 UI

채팅 메시지 렌더링 템플릿

채팅 메시지 목록을 렌더링하는 코드는 message_new 뷰에서도 필요하기에, chat/room_detail.html 템플릿에서 채팅 메시지 목록을 렌더링하는 코드를 chat/_message_list.html 템플릿으로 분리합니다.

chat/templates/chat/_message_list.html 파일 생성
1{% for message in message_list %}
2    <div>[{{ message.role }}] : {{ message.content }}</div>
3{% endfor %}

HTMX를 활용한 채팅 메시지 전송 및 응답 화면 렌더링

채팅 메시지 목록 렌더링 부분을 {% include "chat/_message_list.html" %} 템플릿 태그 호출로 대체합니다. with를 통해 지정 템플릿에서 사용할 값을 추가로 전달할 수 있습니다.

chat/base.html 템플릿에서는 이미 HTMX 라이브러리를 포함하고 있습니다. <form> 태그에서는 htmx 라이브러리를 통해 메시지를 전송하고, 응답을 화면에 반영하겠습니다.

  • hx-post: 메시지 전송 요청 URL

    • HTMX에서는 action 속성을 사용하지 않습니다. hx-요청방식 속성을 통해 요청 URL을 지정합니다.

  • hx-target: 응답을 반영할 태그 ID

    • HTMX에서는 항상 서버로부터 HTML 응답을 받고, hx-target 속성으로 지정한 요소에 응답을 반영합니다.

  • hx-swap: 응답을 반영하는 방법

    • innerHTML: 요소의 내용을 대체합니다. (디폴트)

    • outerHTML: 요소 자체를 대체합니다.

    • beforeend: 요소의 마지막에 응답을 추가합니다. (채팅 UI에 적합)

    • afterend: 요소의 다음에 응답을 추가합니다.

    • delete: 요소를 삭제합니다.

    • none: 응답을 무시합니다.

  • hx-on::before-request: 요청 전에 실행할 자바스크립트 코드

    • this.reset() 코드를 호출하여 폼 요소를 초기화시킵니다.

  • hx-on::after-request: 요청 후에 실행할 자바스크립트 코드

chat/templates/chat/room_detail.html 파일 덮어쓰기
 1{% extends "chat/base.html" %}
 2
 3{% block content %}
 4<div class="flex flex-col h-[calc(100vh-16rem)]">
 5    <div class="bg-white rounded-lg shadow-md p-4 mb-4">
 6        <h1 class="text-2xl font-bold text-gray-800">{{ room.name }}</h1>
 7        <p class="text-sm text-gray-600">생성일: {{ room.created_at|date:"Y-m-d H:i" }}</p>
 8    </div>
 9
10    <div id="messages-container">
11        <div id="chat-messages">
12            {% include "chat/_message_list.html" with message_list=message_list only %}
13        </div>
14    </div>
15
16    <form hx-post="{% url 'chat:message_new' room_pk=room.pk %}"
17          hx-target="#chat-messages"
18          hx-swap="beforeend"
19          hx-on::before-request="this.reset()"
20          novalidate>
21        {% csrf_token %}
22        <div class="flex gap-2">
23            <input type="text" name="content" required autocomplete="off" placeholder="메시지를 입력하세요..." autofocus
24                class="flex-1 bg-gray-100 rounded-lg px-4 py-2">
25            <button type="submit"
26                class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition-colors duration-300">
27                전송
28            </button>
29        </div>
30    </form>
31</div>
32{% endblock %}

채팅/AI 메시지 저장 후에 페이지 이동없이 HTML 응답

HTMX에서는 처리 결과에 대한 HTML 응답을 요구하므로, 채팅/AI 메시지 저장 후에 방금 저장한 채팅/AI 메시지에 대한 HTML 응답을 chat/_message_list.html 템플릿을 통해 생성하여 응답합니다. 그럼 페이지 전환없이 HTMX에 의해 #chat-messages 요소 끝에 방금 저장한 채팅/AI 메시지가 추가됩니다.

chat/views.py 파일 수정
 1@require_POST
 2def message_new(request, room_pk):
 3    room = get_object_or_404(Room, pk=room_pk)
 4
 5    form = MessageForm(data=request.POST, files=request.FILES)
 6    if form.is_valid():
 7        message = form.save(commit=False)
 8        message.room = room
 9        message.save()
10        ai_message = message.create_ai_message()
11        # return redirect("chat:room_detail", pk=room_pk)
12        return render(
13            request,
14            "chat/_message_list.html",
15            {"message_list": [message, ai_message]},
16        )
17
18    return render(
19        request,
20        "chat/message_form.html",
21        {
22            "room": room,
23            "form": form,
24        },
25    )

동작 화면

채팅 메시지를 입력해보세요. 이전과는 다른 점이 느껴지시나요?

../../_images/play1.gif

이번에는 페이지 전환이 발생하지 않았습니다.

새로운 채팅 메시지를 /chat/1/messages/new/ 주소로 POST 방식으로 보냈구요. 서버에서 AI 응답 생성 후에 유저/AI 메시지 목록을 포함한 HTML 응답을 상태코드 200으로 응답했습니다.

이에 HTMX 라이브러리는 서버 응답을 받아 #chat-messages 요소 끝에 추가했습니다.

서버 로그를 보시면 아래와 같이 1개의 요청으로만 동작함을 확인하실 수 있습니다.

[28/Feb/2025 06:19:03] "POST /chat/1/messages/new/ HTTP/1.1" 200 349