HTMX로 쉽게 만들자

본 글은 Claude 4 Sonnet으로 작성했습니다.


HTMX의 장점

1. JavaScript 없이도 동적 웹 구현: HTMX는 HTML 속성만으로 AJAX 요청, DOM 조작, WebSocket 통신이 가능하다. 복잡한 JavaScript 프레임워크 없이도 현대적인 웹 애플리케이션을 만들 수 있다.

<!-- 버튼 클릭 시 서버에서 데이터를 가져와 div에 삽입 -->
<button hx-get="/api/users" hx-target="#user-list">
    사용자 목록 불러오기
</button>
<div id="user-list"></div>

2. 서버 중심 아키텍처 복귀: React나 Vue와 달리 서버에서 HTML을 렌더링하고 클라이언트는 단순히 받아서 표시한다. 이는 SEO에 유리하고 초기 로딩 속도를 향상시킨다.

3. 점진적 향상(Progressive Enhancement): 기존 HTML에 HTMX 속성을 추가하는 방식으로 점진적으로 기능을 확장할 수 있다. 레거시 코드와의 호환성이 우수하다.

4. 작은 번들 사이즈: HTMX는 약 14KB 크기로 React(42KB) + React-DOM(130KB)보다 훨씬 가볍다. 모바일 환경에서 빠른 로딩이 가능하다.

HTMX 핵심 문법 구조

기본 HTTP 요청 속성

<!-- GET 요청: 데이터 조회 -->
<div hx-get="/api/posts" hx-trigger="load">
    <!-- 페이지 로드 시 자동으로 게시글 목록을 가져옴 -->
</div>

<!-- POST 요청: 데이터 전송 -->
<form hx-post="/api/posts" hx-target="#result">
    <input name="title" placeholder="제목" />
    <input name="content" placeholder="내용" />
    <!-- 폼 제출 시 POST 요청을 보내고 결과를 #result에 표시 -->
    <button type="submit">게시글 작성</button>
</form>

<!-- PUT 요청: 데이터 수정 -->
<button hx-put="/api/posts/123" hx-include="#edit-form">
    수정하기
</button>

<!-- DELETE 요청: 데이터 삭제 -->
<button hx-delete="/api/posts/123" 
        hx-confirm="정말 삭제하시겠습니까?">
    삭제하기
</button>

조건부 렌더링

HTMX는 서버 응답에 따라 조건부로 콘텐츠를 표시한다.

<!-- 서버에서 빈 응답이 오면 해당 요소를 숨김 -->
<div hx-get="/api/notifications" 
     hx-trigger="every 30s"
     hx-swap="innerHTML">
    <!-- 30초마다 알림을 확인하고 업데이트 -->
</div>

서버 측 조건부 응답 예시:

# Flask 예시
@app.route('/api/notifications')
def get_notifications():
    notifications = get_user_notifications()
    if not notifications:
        return ""  # 빈 응답으로 요소 숨김
    
    return render_template('notifications.html', 
                         notifications=notifications)

반복 요소 처리

목록 데이터는 서버에서 반복 렌더링하여 전달한다.

<!-- 사용자 목록 컨테이너 -->
<div id="user-list" 
     hx-get="/api/users" 
     hx-trigger="load">
    <!-- 서버에서 렌더링된 사용자 목록이 여기에 삽입됨 -->
</div>

<!-- 무한 스크롤 구현 -->
<div hx-get="/api/posts?page=1" 
     hx-trigger="load"
     hx-swap="afterend">
    <!-- 첫 페이지 로드 -->
</div>

<div hx-get="/api/posts?page=2" 
     hx-trigger="revealed"
     hx-swap="afterend">
    <!-- 스크롤하여 요소가 보일 때 다음 페이지 로드 -->
</div>

서버 측 반복 처리:

@app.route('/api/users')
def get_users():
    users = User.query.all()
    # 서버에서 HTML로 렌더링하여 반환
    return render_template('user_list.html', users=users)
<!-- user_list.html 템플릿 -->
{% for user in users %}
<div class="user-card">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <!-- 각 사용자별 삭제 버튼 -->
    <button hx-delete="/api/users/{{ user.id }}" 
            hx-target="closest .user-card"
            hx-swap="outerHTML">
        삭제
    </button>
</div>
{% endfor %}

이벤트 트리거와 타겟 지정

<!-- 다양한 트리거 이벤트 -->
<input hx-get="/api/search" 
       hx-trigger="keyup changed delay:500ms"
       hx-target="#search-results"
       name="query"
       placeholder="검색어 입력">
<!-- 키업 이벤트 발생 후 500ms 지연하여 검색 실행 -->

<!-- 여러 타겟에 동시 업데이트 -->
<button hx-post="/api/cart/add" 
        hx-target="#cart-items"
        hx-swap="beforeend">
    <!-- 장바구니에 아이템 추가 후 목록 끝에 추가 -->
    장바구니 추가
</button>

<!-- 조건부 CSS 클래스 토글 -->
<button hx-post="/api/like" 
        hx-target="#like-count"
        hx-swap="innerHTML"
        onclick="this.classList.toggle('liked')">
    <!-- 좋아요 버튼 클릭 시 카운트 업데이트 및 스타일 변경 -->
    ♥ <span id="like-count">0</span>
</button>

폼 검증과 에러 처리

<form hx-post="/api/register" hx-target="#form-result">
    <input name="email" type="email" required>
    <input name="password" type="password" required>
    
    <!-- 제출 중 표시될 인디케이터 -->
    <button type="submit">
        가입하기
        <span class="htmx-indicator">처리 중...</span>
    </button>
    
    <!-- 서버 응답이 표시될 영역 -->
    <div id="form-result"></div>
</form>

서버 측 검증 처리:

@app.route('/api/register', methods=['POST'])
def register():
    email = request.form.get('email')
    password = request.form.get('password')
    
    # 검증 실패 시
    if User.query.filter_by(email=email).first():
        return '<div class="error">이미 존재하는 이메일입니다.</div>', 400
    
    # 성공 시
    create_user(email, password)
    return '<div class="success">가입이 완료되었습니다!</div>'

WebSocket 통신

<!-- WebSocket 연결로 실시간 채팅 -->
<div hx-ws="connect:/ws/chat">
    <div id="messages" hx-ws="swap"></div>
    <!-- WebSocket 메시지가 여기에 자동 삽입 -->
    
    <form hx-ws="send:submit">
        <input name="message" placeholder="메시지 입력">
        <button type="submit">전송</button>
    </form>
</div>

실무 활용 팁

1. 로딩 상태 표시

<button hx-get="/api/data" hx-indicator="#spinner">
    데이터 로드
</button>
<div id="spinner" class="htmx-indicator">로딩 중...</div>

2. 캐시 제어

<!-- 매번 새로운 데이터 요청 -->
<div hx-get="/api/time" 
     hx-trigger="every 1s"
     hx-headers='{"Cache-Control": "no-cache"}'>
</div>

3. 에러 처리

<div hx-get="/api/data" 
     hx-on="htmx:responseError: alert('서버 오류가 발생했습니다.')">
</div>

HTMX는 간단한 HTML 속성만으로도 강력한 인터랙티브 웹 애플리케이션을 구축할 수 있게 해주는 도구다. 복잡한 프론트엔드 프레임워크에 지친 개발자들에게는 새로운 대안이 될 수 있다.