Skip to content

Split View: HTMX & Hyperscript — 反-SPA 진영의 2026, Carson Gross가 던진 하이퍼미디어 명제 심층 (2026)

|

HTMX & Hyperscript — 反-SPA 진영의 2026, Carson Gross가 던진 하이퍼미디어 명제 심층 (2026)

프롤로그 — SPA 피로, 그리고 Carson의 한 문장

2026년 5월, 프런트엔드 개발자 한 사람의 평균적인 하루를 그려보자.

pnpm install이 끝나기를 기다리는 동안 커피를 한 잔 더 끓이고, Vite dev server를 띄우고, React Query 캐시 무효화 정책을 다시 짠다. 어제 잘 돌던 빌드는 오늘 Module not found: Can't resolve 'crypto'로 죽었고, 누군가 next.config.js에 webpack 폴리필을 추가했더니 이번에는 Turbopack이 화를 낸다. 폼 하나를 만드는 데 — useState, useEffect, useMutation, zod 스키마, react-hook-form 리졸버, optimistic update 로직, 에러 바운더리, 로딩 스피너 — 일곱 개의 추상이 동원된다. 그 폼이 하는 일은 결국 POST /todos이고, 응답을 받아 화면을 갱신하는 것이다.

이 익숙한 풍경에 한 사람이 칼을 꽂았다. 몽고메리 대학 컴퓨터 과학과 교수, Carson Gross. 그는 2020년 intercooler.js를 다시 쓴 라이브러리에 HTMX라는 이름을 붙였고, 한 문장으로 시대를 요약했다.

"HTML은 이미 하이퍼미디어다. 우리가 한 일은 그 잠재력을 11KB로 풀어준 것뿐이다."

HTMX는 새로운 프레임워크가 아니다. HTML에 네 개의 속성을 추가했을 뿐이다. hx-get, hx-post, hx-swap, hx-target. 폼은 더 이상 JavaScript 핸들러를 필요로 하지 않는다. 버튼이 직접 서버에 POST를 보내고, 서버는 HTML 조각을 돌려주며, 그 조각이 페이지의 일부를 교체한다. 그것뿐이다.

그러나 그 단순함은 단순한 문법 이상의 무언가다. Carson은 그 위에 책 한 권을 얹었다. 『Hypermedia Systems』 — Roy Fielding의 REST 논문을 다시 읽고, HATEOAS를 진지하게 받아들이며, "왜 우리는 JSON API를 만들고, 그 위에 클라이언트 사이드 라우터를 만들고, 그 위에 상태 관리를 만들고, 그 위에 캐싱을 만들고, 그 위에 최적화 컴파일러를 만들었는가"를 묻는 책이다. 답은 도발적이다. "필요하지 않았다."

이 글은 HTMX 진영 — 反-SPA 진영, 또는 "boring web" 르네상스 — 의 2026년을 정리한다. HTMX 2.x의 코어, _hyperscript라는 Carson의 두 번째 작품, Datastar라는 차세대 후보, Alpine.js라는 타협안, 『Hypermedia Systems』가 제시한 사고 프레임, 37signals와 같은 프로덕션 사례, 그리고 — 가장 중요하게 — HTMX가 틀린 곳까지.

이 글은 HTMX 옹호문이 아니다. 언제 HTMX가 옳고 언제 React가 옳은지 결정하는 데 필요한 모든 도구를 한 호흡으로 제공하는 것이 목표다.


1장 · 하이퍼미디어 명제 — Roy Fielding을 다시 읽는다

HTMX의 모든 주장은 한 권의 박사 논문에서 시작된다. 2000년, Roy Fielding의 "Architectural Styles and the Design of Network-based Software Architectures" — 우리가 줄여서 REST라고 부르는 그것이다.

Fielding은 REST에 여섯 개의 제약을 두었다. 클라이언트-서버, 무상태, 캐시 가능, 통일된 인터페이스, 계층화된 시스템, 그리고 — HATEOAS: Hypermedia As The Engine Of Application State.

마지막 제약이 핵심이다. HATEOAS는 "클라이언트는 다음 행동에 대한 정보를 서버가 응답으로 보내준 하이퍼미디어로부터만 얻어야 한다"고 말한다. 즉, 응답에 "이 다음에 할 수 있는 것은 이러이러하다"는 정보가 링크와 폼의 형태로 들어 있어야 한다는 것이다.

Carson의 진단은 이렇다.

  • 오늘날 우리가 REST라고 부르는 것의 99%는 HATEOAS를 제외한 5개의 제약만 따른다.
  • JSON API는 데이터만 주고 행동을 주지 않는다. 그래서 클라이언트가 "다음에 무엇을 할 수 있는가"를 코드로 적어 두어야 한다.
  • 그 결과 클라이언트는 도메인 모델의 복제본을 갖게 되고, 서버와 클라이언트가 두 번 동기화를 해야 하는 시스템이 된다.
  • 반면 HTML은 그 자체로 하이퍼미디어다. <form> 태그는 행동을 포함하고, <a> 태그는 상태 전이를 포함한다.

HTMX의 명제는 그래서 단순하다.

"브라우저는 이미 우수한 하이퍼미디어 클라이언트다. HTMX는 그 클라이언트의 어휘를 확장한다."

기본 HTML은 "링크 클릭" 또는 "폼 제출" 시에만 서버 요청이 일어난다. HTMX는 그것을 확장한다.

  • 어떤 이벤트도 트리거가 될 수 있다 (hx-trigger)
  • 어떤 HTTP 동사도 사용할 수 있다 (hx-get, hx-post, hx-put, hx-patch, hx-delete)
  • 응답을 페이지의 어느 부분에든 교체할 수 있다 (hx-target, hx-swap)

이것이 HTMX의 전부다. 한 줄로 요약하면 — "any element, any event, any HTTP verb, any swap target."


2장 · HTMX 2.x 코어 — 네 개의 속성으로 99%를 만든다

2024년 1월, HTMX 2.0이 GA로 나왔다. 2025년에 2.1, 2026년 초에 2.2가 나왔고, 현재 안정 버전은 htmx.org 기준 11.4 KB(gzipped). 의존성은 zero. IE11 지원은 1.x에 남기고 2.x는 evergreen 브라우저 전용으로 단순화되었다.

2.1 설치 — script 태그 한 줄

<script src="https://unpkg.com/htmx.org@2.0.4"></script>

이게 전부다. npm 패키지도 있지만, HTMX는 <script> 태그로 시작하는 것을 명시적으로 권장한다. 빌드 스텝 없음, 번들러 없음, 트리 셰이킹 걱정 없음.

2.2 첫 번째 예제 — 검색 폼

전통적인 SPA에서 "타이핑하면 결과가 라이브로 갱신되는 검색"을 만들려면 — useState, useDeferredValue, debounce 훅, 캐시 무효화, abort controller, 로딩 상태 머신 — 최소 50줄의 React 코드가 필요하다.

HTMX 버전:

<input type="search" name="q"
       hx-get="/search"
       hx-trigger="input changed delay:300ms, search"
       hx-target="#results"
       hx-indicator=".spinner" />

<div id="results"></div>
<div class="spinner htmx-indicator">Searching...</div>

여기서 일어나는 일을 한 줄씩 읽어보자.

  • hx-get="/search" — 트리거되면 GET /search?q=...를 보낸다. name="q"가 자동으로 쿼리 파라미터가 된다.
  • hx-trigger="input changed delay:300ms, search"input 이벤트가 발생하고 값이 변경되었으며 300ms 동안 다른 이벤트가 없을 때 트리거. 또는 사용자가 검색 키를 눌렀을 때.
  • hx-target="#results" — 응답으로 #results의 내용을 교체.
  • hx-indicator=".spinner" — 요청 중에 .spinnerhtmx-request 클래스를 추가. CSS로 표시/숨김 처리.

서버는? Express, FastAPI, Django, Rails, Phoenix, Spring — 무엇이든 HTML 조각만 돌려주면 된다.

@app.get("/search")
def search(q: str):
    results = db.search(q)
    return render_template("results_fragment.html", results=results)

이 패턴이 갖는 의미는 깊다. 로딩 상태, debounce, abort, race condition, 캐싱 — 이 모든 것을 HTMX가 처리한다. 우리는 비즈니스 로직만 적는다.

2.3 코어 속성 13개

HTMX의 표면적은 작다. 13개의 핵심 속성으로 거의 모든 것이 표현된다.

속성역할
hx-get / hx-post / hx-put / hx-patch / hx-deleteHTTP 요청 발사
hx-trigger어떤 이벤트로 트리거할지 (click, input, every 2s, revealed, ...)
hx-target응답을 어느 엘리먼트에 적용할지 (CSS 선택자)
hx-swap어떻게 적용할지 (innerHTML, outerHTML, beforebegin, afterend, ...)
hx-vals요청에 추가할 값 (JSON 또는 JS 함수)
hx-headers커스텀 헤더
hx-indicator요청 중 인디케이터
hx-confirm전송 전 confirm() 다이얼로그
hx-push-url응답을 받을 때 브라우저 URL 갱신
hx-boost일반 링크/폼을 AJAX로 자동 업그레이드

hx-boost만으로도 흥미롭다. <body hx-boost="true"> 한 줄을 추가하면, 페이지의 모든 <a><form>이 자동으로 AJAX 요청을 보내고, 응답에서 body 부분만 교체한다. Turbo·Inertia와 비슷한 효과를 한 속성으로.

2.4 hx-swap의 위력 — DOM 조작이 선언적이 된다

hx-swap은 응답 HTML을 DOM에 적용하는 방법을 결정한다.

  • innerHTML (기본): 타깃의 내부를 교체
  • outerHTML: 타깃 자체를 교체
  • beforebegin / afterbegin / beforeend / afterend: 인접 위치에 삽입
  • delete: 타깃 삭제 (응답 무시)
  • none: DOM 변경 없음 (사이드 이펙트만)

여기에 hx-swap="outerHTML swap:1s settle:1s scroll:#top" 같은 modifier를 붙이면 — swap 전 1초 대기(애니메이션), settle 단계 1초(transition class 적용), 완료 후 #top으로 스크롤 — 이 모든 것이 한 줄에 표현된다.

2.5 hx-trigger의 위력 — 이벤트 DSL

hx-trigger는 작은 DSL이다.

<!-- 5초마다 자동 갱신 -->
<div hx-get="/notifications" hx-trigger="every 5s"></div>

<!-- 뷰포트에 들어오면 한 번만 트리거 (lazy load) -->
<div hx-get="/heavy-section" hx-trigger="revealed"></div>

<!-- 다른 엘리먼트의 이벤트로 트리거 -->
<div hx-get="/related" hx-trigger="updated from:#article"></div>

<!-- 키보드 단축키 -->
<form hx-post="/save" hx-trigger="keyup[ctrlKey&&key=='s'] from:body"></form>

every Ns로 폴링이 한 줄이 된다. revealed로 무한 스크롤이 한 줄이 된다. from:#selector로 다른 엘리먼트의 이벤트를 listen한다. WebSocket 없이도 polling이 충분한 영역에서 — 알림, 주식 시세, 라이브 카운터 — HTMX는 즉시 답을 준다.


3장 · Hyperscript — Carson의 두 번째 도발

HTMX는 서버 왕복으로 처리할 수 있는 일을 다룬다. 그러나 "버튼을 클릭하면 옆에 있는 div의 클래스를 토글한다" 같은 순수 클라이언트 사이드 동작도 우리는 자주 필요하다. 이때 React를 끌어오기는 과한다. jQuery는 너무 옛날 같다.

Carson은 이 빈자리에 _hyperscript (밑줄로 시작, 줄임말 _hs)를 놓았다. 영어 문장처럼 읽히는 이벤트 핸들러 DSL.

<button _="on click toggle .active on #panel">Toggle</button>

문장으로 읽어보자. "on click, toggle the class .active on #panel." 영어 그대로다. 파서는 이걸 그대로 받아들인다. HyperTalk(HyperCard의 스크립트 언어)에서 영감을 받았다.

3.1 더 복잡한 예

<input _="on input
            if my value's length < 3
              hide #suggestions
            else
              show #suggestions
              put 'Searching...' into #suggestions
            end" />

이게 무엇이냐. 입력 길이가 3 미만이면 #suggestions를 숨기고, 아니면 보이고, 텍스트를 "Searching..."으로 채운다. 코드는 그대로 영어다.

3.2 fetch 한 줄 비동기

<button _="on click
            fetch /api/heavy
            put it into #result">
  Load
</button>

fetch /api/heavy 다음 줄의 it은 응답을 가리킨다. put it into #result — 응답을 #result에 넣어라. JavaScript Promise나 async/await의 이질감 없이 자연스럽게 흐른다.

3.3 자주 쓰이는 패턴

  • 토글 클래스: on click toggle .open on .menu
  • 컨퍼메이션: on click if confirm('Sure?') trigger submit on me
  • 타이머: on load wait 3s then add .visible to #toast
  • 드래그: on mousedown set $dragging to me on mousemove if $dragging then ...
  • 이벤트 위임: on click from .row in me ...

3.4 Hyperscript 채택 현실 — 2026년의 답

솔직해지자. Hyperscript는 HTMX만큼 채택되지 않았다. GitHub 스타는 HTMX의 1/10 수준이다. 그 이유는 단순하다.

  • VS Code 신택스 하이라이팅이 빈약하다 — 따옴표 안의 문자열로 인식되어 색이 나오지 않는다.
  • 에러 메시지가 부족하다 — 영어 문장이라 어디서 틀렸는지 추적이 어렵다.
  • TypeScript와 통합 안 됨 — 타입 안전성 없음.
  • 알파인이 더 친숙하다 — JavaScript 스타일 표현식이라 React/Vue 출신자가 적응 빠르다.

그래서 HTMX 사용자의 다수는 Hyperscript 대신 Alpine.js를 함께 쓴다. 그것이 다음 장이다.


4장 · Alpine.js — 반응성을 원하는 자를 위한 다리

Caleb Porzio가 만든 Alpine.js는 "Vue 3의 reactivity, jQuery의 직접성, Tailwind의 inline 접근"을 합친 라이브러리다. 15 KB. 의존성 zero. HTMX와 가장 잘 어울리는 보완재로 자리 잡았다.

4.1 기본

<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open" x-transition>Hello!</div>
</div>
  • x-data — 컴포넌트 스코프를 열고 reactive state 객체를 정의
  • @click (또는 x-on:click) — 이벤트 핸들러
  • x-show — 조건부 렌더링 (display 토글)
  • x-transition — 자동 트랜지션

이 다섯 개의 directive로 90%가 표현된다. 추가로 x-text, x-html, x-bind, x-model, x-for, x-if 등이 있다.

4.2 HTMX + Alpine — 황금 조합

HTMX는 서버 왕복, Alpine은 로컬 UI 상태. 둘은 충돌하지 않고 자연스럽게 합쳐진다.

<div x-data="{ mode: 'list' }">
  <button @click="mode = 'list'" :class="{ 'active': mode === 'list' }">List</button>
  <button @click="mode = 'grid'" :class="{ 'active': mode === 'grid' }">Grid</button>

  <div hx-get="/items" hx-trigger="load" hx-target="this">
    <!-- 서버에서 받은 HTML이 들어옴 -->
  </div>
</div>

뷰 모드 토글(클라이언트 상태)은 Alpine, 아이템 로드(서버 상태)는 HTMX. 각자 잘하는 일을 한다.

4.3 alpine-ajax — 동전의 반대편

흥미롭게도 Alpine 진영에는 alpine-ajax라는 서드파티 플러그인이 있다. Alpine 위에서 HTMX 같은 서버 통신을 한다. 이 도구의 존재 자체가 메시지다. HTMX 명제와 Alpine 명제는 같은 것을 다른 진입점에서 본 결과다.


5장 · Datastar — 차세대 하이퍼미디어 프레임워크

Delaney Gillilan이 만든 Datastar는 HTMX + Alpine을 한 라이브러리로 합치겠다는 도발적 시도다. 2024년에 1.0이 나왔고, 2026년 현재 1.3이 안정 버전. 사이즈는 ~10 KB.

5.1 한 가지 핵심 차이 — SSE 우선

HTMX는 일반 HTTP 응답을 받아 DOM을 교체한다. Datastar는 Server-Sent Events(SSE)를 1급 시민으로 둔다. 즉, 하나의 요청이 여러 개의 fragment 업데이트를 스트림으로 받는다.

<button data-on-click="@get('/calculate')">
  Calculate
</button>

<div id="step1"></div>
<div id="step2"></div>
<div id="final"></div>

서버는 단일 요청을 받고 SSE로 응답한다.

event: datastar-fragment
data: selector #step1
data: fragments <div id="step1">Step 1 done</div>

event: datastar-fragment
data: selector #step2
data: fragments <div id="step2">Step 2 done</div>

event: datastar-fragment
data: selector #final
data: fragments <div id="final">Result: 42</div>

LLM 스트리밍, 진행 상황 표시, 라이브 대시보드 — Datastar의 본진은 이런 곳이다. HTMX의 htmx-ext-sse 확장으로도 가능하지만, Datastar는 처음부터 SSE를 가정하고 설계되었다.

5.2 표현식 — Alpine 스타일

<input data-bind-name />
<div data-text="$name"></div>
<button data-on-click="$count++">+</button>
<p data-text="$count"></p>

data-bind-*, data-text, data-on-* — Alpine과 거의 호환되는 어휘. signal 기반 reactivity를 채용했고, 모든 state는 $로 접근한다.

5.3 Datastar vs HTMX — 어떻게 선택할까

  • LLM 스트리밍/실시간 대시보드 중심 → Datastar (SSE 1급)
  • 전통적 CRUD/폼 중심 → HTMX (생태계 성숙, 호환성 우월)
  • 클라이언트 사이드 상태가 적지 않음 → Datastar (Alpine 기능 내장)
  • 서버 백엔드와 잘 통합된 라이브러리 풍부 → HTMX (Django, Rails, FastAPI, Express 모두 헬퍼 있음)

2026년 현재 HTMX가 압도적 다수다. Datastar는 "SSE 네이티브"라는 강점을 LLM 시대에 들고 들어왔지만, 채택은 아직 얼리어답터 단계다.


6장 · Hypermedia Systems — 책 한 권이 만든 사상 체계

Carson Gross, Adam Stepinski, Deniz Akşimşek 세 사람이 쓴 『Hypermedia Systems』는 2023년에 출간되어 GitHub에서 오픈 액세스로 읽을 수 있다. 책의 부제는 "HTML, HTTP, and the Hypermedia Architecture"이다.

6.1 책이 답하는 질문들

  • 왜 우리는 JSON API + JavaScript 클라이언트 패턴을 기본값으로 받아들였는가?
  • HATEOAS가 정말 실용적인가, 아니면 학술적 호기심에 불과한가?
  • 모바일에서 하이퍼미디어가 통할 수 있는가? (책은 "Hyperview"라는 안드로이드/iOS 하이퍼미디어 클라이언트도 다룬다)
  • 하이퍼미디어로 만든 시스템은 어떻게 확장하는가?

6.2 LoB — Locality of Behavior 원칙

책이 도입한 핵심 원칙 중 하나가 Locality of Behavior(LoB, 행동의 지역성) 다. 한 문장으로 — "엘리먼트의 동작은 그 엘리먼트를 보면 알 수 있어야 한다."

React는 행동을 컴포넌트 트리 어딘가의 핸들러로 분리한다. CSS는 외부 파일에 둔다. JS 이벤트 위임도 행동을 다른 곳에 둔다. 이것이 좋다고 우리는 배워 왔지만(Separation of Concerns), Carson은 다른 원칙을 제안한다. 분리된 관심사가 아니라, 모인 행동.

<!-- 한 곳에서 행동이 보인다 -->
<button hx-post="/like"
        hx-target="#count"
        _="on click toggle .liked on me">
  Like
</button>

이 버튼이 무엇을 하는지 — POST 요청, 카운트 갱신, 클래스 토글 — 다른 파일을 열 필요가 없다. 디자이너도, 백엔드 개발자도, AI 어시스턴트도 이 한 줄을 보고 이해한다.

6.3 SoC vs LoB — 어느 쪽이 옳은가

답은 둘 다 옳고, 맥락이 결정한다. 큰 React 앱에서 컴포넌트 추상은 LoB의 결여를 보상한다. HTMX 앱에서는 화면이 작아 LoB가 그대로 통한다. 사용자가 5명인 내부 도구에서 LoB를 따르고, 1억 명을 향하는 SaaS에서는 컴포넌트 추상이 필요할 수 있다.

핵심은 — LoB는 기본값이 되어야 하고, 추상은 정당화되어야 한다는 것이다.


7장 · 프로덕션 사례 — 누가 실제로 쓰는가

HTMX는 장난감이 아니다. 2026년 현재 다음 사례들이 공개되어 있다.

  • GitHub — 새 PR 페이지의 일부, 이슈 코멘트 — Turbo 위주이지만 HTMX 영감 패턴이 다수
  • NASA JPL — 내부 도구 다수가 HTMX 기반(공개된 컨퍼런스 발표)
  • Contexte — 프랑스의 정치 뉴스 사이트, HTMX로 전체 리뉴얼
  • Replicant.au — 호주의 인디 SaaS, HTMX + Hyperscript 전면 사용
  • David Heinemeier Hansson37signals — Hotwire(Turbo + Stimulus)가 메인이지만, "프런트엔드 단순화" 진영의 대표 주자로 HTMX 진영과 사상을 공유
  • Bunny.net — CDN 회사, 관리자 대시보드 HTMX 기반
  • Quanta Magazine — 일부 인터랙티브 콘텐츠

DHH(David Heinemeier Hansson)는 HTMX 사용자는 아니지만, "복잡함은 패션이 아니다"라는 캠페인의 가장 큰 목소리다. 그의 Hotwire는 Rails 진영의 HTMX이고, 사상은 거의 같다. 2024-2025년 그가 한 일련의 토크 — "The One Person Framework", "What is Modern Web Development?" — 는 HTMX 진영의 자료로도 인용된다.

7.1 회사 규모는 어디까지 가능한가

소규모 SaaS, 내부 도구, 컨텐츠 사이트, 어드민 패널, CMS — 여기까지는 HTMX가 명백히 강하다. 모바일 앱처럼 동작해야 하는 PWA, 오프라인이 1급 시민인 협업 도구, 카운슬 of WebGL을 띄우는 클라이언트 헤비 게임 — 이런 곳에서는 HTMX가 약하다.

37signals의 Basecamp는 둘 사이 어딘가에 있다. 협업 도구이지만, 데이터 동기화 같은 무거운 클라이언트 상태는 적다.


8장 · 정직한 약점 — HTMX가 틀리는 곳

옹호문이 아니라고 했다. HTMX는 다음 영역에서 약하거나, 맞지 않는다.

8.1 풍부한 클라이언트 사이드 상태

  • Figma 같은 협업 화이트보드 — 수많은 객체의 상태를 클라이언트에서 추적해야 한다. 서버 왕복은 비현실적이다.
  • Notion 같은 블록 에디터 — 키 입력마다 서버에 가는 것은 불가능하다.
  • VS Code Web — 에디터 상태, 신택스 트리, LSP 응답 — 모두 클라이언트에서 처리되어야 한다.

이런 곳에서는 React/Vue/Solid가 옳다.

8.2 오프라인 우선

PWA, 모바일 우선, 비행기에서도 동작해야 하는 앱. HTMX는 서버 응답을 가정하므로 오프라인이 깨진다. Service Worker + IndexedDB + 동기화 큐 — 이 조합은 SPA의 영역이다.

8.3 복잡한 클라이언트 사이드 애니메이션

  • 60fps 캔버스 그림
  • 물리 시뮬레이션
  • 3D 인터랙션
  • 시간축 기반 비디오 편집

이런 곳에서 HTMX는 도울 게 거의 없다.

8.4 서버 부하

HTMX는 매 인터랙션마다 서버를 친다. 트래픽 패턴이 정적 자산 중심이 아니라 동적 HTML 생성 중심이 된다. 서버 측 캐싱, edge rendering, CDN 전략이 필수다. SPA에서는 이 부담의 일부가 CDN과 클라이언트로 옮겨갔지만, HTMX에서는 서버가 그것을 다시 받아야 한다.

8.5 모바일 네트워크

응답 시간이 200ms 이상 늘어지면 사용자 경험이 무너진다. Optimistic update 패턴이 어렵고, "버튼을 눌렀는데 뭐가 변한 게 없다"는 인상을 주기 쉽다.

해법은 있다. hx-disabled-elt, hx-indicator, 로컬 Alpine 상태로 즉시 UI 갱신 후 서버 응답으로 확정 — 그러나 "기본값으로 빠른" SPA보다는 신경을 더 써야 한다.


9장 · 결정 프레임워크 — 언제 HTMX인가

다음 표를 기준선으로 삼자. 모든 항목을 합산해 점수가 0보다 크면 HTMX, 0보다 작으면 SPA.

질문HTMX 점수SPA 점수
폼 중심인가?+20
콘텐츠 중심인가?+20
내부 도구/어드민인가?+20
팀이 5명 이하인가?+10
서버 언어가 강한가 (Django/Rails/Phoenix)?+20
SEO가 중요한가?+1-1
오프라인이 필요한가?-2+2
클라이언트 사이드 상태가 많은가?-2+2
60fps 애니메이션이 핵심인가?-2+2
모바일 앱과 코드 공유가 필요한가?-1+2
5만명 이상 동시 사용자인가?-1+1
디자이너가 HTML/CSS 우선인가?+10
기존 코드베이스가 React인가?-2+1

이 점수표는 절대값이 아니다. 그러나 시작점은 된다. 점수가 0 부근이면 — 두 패턴을 섞을 수 있다. HTMX로 큰 영역을 만들고, 진짜 SPA가 필요한 부분(에디터, 화이트보드)만 React island로 묶는 패턴이 갈수록 흔하다.

9.1 마이그레이션 전략

기존 React 앱이 있는데 HTMX로 옮기고 싶다면 — 전체 재작성은 거의 항상 잘못된 답이다. Strangler Fig 패턴을 쓰자.

  1. 새 페이지부터 HTMX로 작성 — 신규 어드민 페이지, 새 모듈
  2. CRUD가 단순한 화면을 표적 이전 — 폼 페이지, 리스트 페이지
  3. 에디터/대시보드 등 클라이언트 헤비 화면은 React로 유지 — island로 격리
  4. 공유 헤더/네비게이션은 서버 사이드 렌더링으로 — HTMX hx-boost로 전환 가속

10장 · 코드 한 페이지로 보는 HTMX 진영

10.1 HTMX — 인플레이스 편집 폼

<!-- 표시 모드 -->
<div hx-target="this" hx-swap="outerHTML">
  <p>제목: 안녕, 세상</p>
  <button hx-get="/posts/1/edit">편집</button>
</div>

<!-- 서버 응답: 편집 모드 -->
<form hx-put="/posts/1" hx-target="this" hx-swap="outerHTML">
  <input name="title" value="안녕, 세상" />
  <button type="submit">저장</button>
  <button hx-get="/posts/1">취소</button>
</form>

서버 측 의사 코드(Python/FastAPI):

@app.get("/posts/{id}/edit")
def edit_form(id: int):
    post = db.get_post(id)
    return templates.TemplateResponse("post_edit.html", {"post": post})

@app.put("/posts/{id}")
def update_post(id: int, title: str = Form(...)):
    db.update_post(id, title=title)
    post = db.get_post(id)
    return templates.TemplateResponse("post_display.html", {"post": post})

화면 전환 로직, 상태 머신, 폼 라이브러리 — 모두 사라졌다.

10.2 Hyperscript — 모달 닫기 한 줄

<div id="modal" class="hidden">
  <div class="overlay" _="on click hide #modal"></div>
  <div class="content">
    <button _="on click hide #modal">닫기</button>
    <p>Esc로도 닫힙니다.</p>
  </div>
</div>

<script>
  document.addEventListener('keydown', e => {
    if (e.key === 'Escape') document.getElementById('modal').classList.add('hidden')
  })
</script>

위 JavaScript도 Hyperscript로 한 줄에 적을 수 있다.

<body _="on keydown[key=='Escape'] from window hide #modal">

10.3 Datastar — LLM 스트리밍

<button data-on-click="@post('/chat', { messages: $messages })">
  Send
</button>

<div id="response"></div>

서버는 SSE로 토큰 단위 fragment를 보낸다.

event: datastar-fragment
data: merge inner
data: selector #response
data: fragments Hello

event: datastar-fragment
data: merge inner
data: selector #response
data: fragments Hello,

event: datastar-fragment
data: merge inner
data: selector #response
data: fragments Hello, world!

Datastar는 이런 사용 사례를 처음부터 가정한다. HTMX에서는 htmx-ext-sse 확장으로 비슷하게 가능하지만 1급 시민이 아니다.


에필로그 — 지루한 웹의 르네상스

웹의 처음 10년은 단순했다. HTML이 있었고, 폼이 있었고, 링크가 있었고, 서버가 있었다. 그러고 jQuery가 왔고, Ajax가 왔고, Backbone과 Angular가 왔고, React가 와서 — 우리가 만들던 모든 것이 갑자기 두 번 그려지기 시작했다. 한 번은 서버에서, 한 번은 클라이언트에서. 한 번은 Python으로, 한 번은 JavaScript로. 그리고 그 둘을 맞추는 데 점점 더 큰 비율의 에너지를 썼다.

Carson Gross는 그 어느 시점에 멈춰서 물었다. "우리가 정말 이 모든 것을 필요로 했는가?"

대답은 — 어떤 경우에는 그렇고, 어떤 경우에는 아니다. HTMX는 모든 곳에서 옳지 않다. 그러나 우리가 R React를 사용했던 곳의 절반은 React가 옳지 않았다. 그 절반의 절반은 — 어드민, CRUD, 사내 도구, 콘텐츠 사이트 — HTMX 한 줄로 충분했다.

2026년에 HTMX 진영이 강해지는 이유는 단순하다. AI 시대에 단순함은 더 가치 있는 자원이 되었다. LLM은 React 컴포넌트 트리 안의 상태 흐름을 추적하는 데 약하지만, <button hx-post="/like">가 무엇을 하는지 즉시 안다. AI 어시스턴트와 협업하는 코드베이스는 LoB가 우월하다.

지루한 웹은 끝난 게 아니다. 그저 잠시 잊혀졌다.

체크리스트 — 다음 프로젝트를 시작하기 전에

  • 폼 중심인가, 데이터 중심인가? 폼 중심이면 HTMX 먼저
  • 서버 언어와 강하게 통합되는가? Django/Rails/Phoenix면 HTMX가 자연스럽다
  • 오프라인이 핵심 요구사항인가? 그렇다면 SPA 또는 PWA
  • 60fps 캔버스/3D가 핵심인가? 그렇다면 SPA
  • 디자이너가 HTML/CSS로 직접 작업하는가? HTMX가 그들에게 친숙하다
  • 팀 규모가 5명 이하인가? 단순함의 ROI가 가장 높다
  • AI 어시스턴트와 짝 프로그래밍하는가? LoB 코드가 더 잘 이해된다

안티 패턴 — 하지 말 것

  • HTMX로 SPA 흉내내기 — 페이지 전환을 모두 hx-boost로만 처리하고 클라이언트 라우터를 만들지 않기. URL이 일관되게 작동하도록 서버 측 라우팅을 유지하라
  • 모든 인터랙션에 서버 왕복 — 클래스 토글 같은 순수 UI 동작은 Hyperscript/Alpine로 처리. 서버를 부를 필요 없는 곳에 부르지 말 것
  • HTML 조각이 거대해짐 — 응답 fragment는 작아야 한다. 페이지 절반을 매번 갱신한다면 SoC가 깨진 신호
  • Hyperscript와 Alpine 동시 사용 — 둘은 비슷한 영역을 다룬다. 하나만 선택하라
  • 테스트 부재 — HTMX는 서버를 더 자주 친다. 통합 테스트(Playwright 같은)가 더 중요해진다
  • 하이퍼미디어 가능성 무시한 JSON API 설계 — REST API를 JSON으로 설계하고 HTMX 페이지를 그 위에 얹으면 두 번 동기화하는 SPA와 같아진다. 하이퍼미디어를 1급 응답으로 두라
  • HTMX 확장을 모르고 시작htmx-ext-sse, htmx-ext-ws, htmx-ext-response-targets 등을 익혀라. 코어가 작은 만큼 확장이 자주 필요하다

다음 글 예고

다음 글에서는 HTMX + Django/FastAPI/Rails 백엔드 스택의 실전 구성 — 템플릿 조각 관리 전략, 인증 흐름, 파일 업로드, 폼 검증, CSRF, 그리고 "HTMX 친화적 컨트롤러 패턴" — 을 다룬다. 그리고 그 다음에는 HTMX와 React island를 한 페이지에 섞는 하이브리드 패턴으로 — Notion 스타일 에디터를 React로 띄우고 그 주변을 HTMX로 감싸는 실전 예제를 가져온다.


참고 / References

HTMX & Hyperscript — The Anti-SPA Camp in 2026, Carson Gross's Hypermedia Thesis Deep Dive (2026)

Prologue — SPA Fatigue, and Carson's One Sentence

Picture an average day for a frontend developer in May 2026.

You wait for pnpm install while making another coffee. You boot Vite dev server. You redesign the React Query cache invalidation policy. Yesterday's build that worked dies today with Module not found: Can't resolve 'crypto'. Someone added a webpack polyfill to next.config.js, so now Turbopack is angry. Building one form drags in seven abstractions — useState, useEffect, useMutation, a zod schema, a react-hook-form resolver, optimistic update logic, error boundaries, loading spinners. What that form actually does is POST /todos and refresh part of the screen.

Into this familiar landscape one person drove a knife. Carson Gross, professor of computer science at Montgomery College, rewrote intercooler.js in 2020, named it HTMX, and summarized an era in one sentence.

"HTML is already hypermedia. All we did was unlock its potential in 11 KB."

HTMX is not a new framework. It adds four attributes to HTML. hx-get, hx-post, hx-swap, hx-target. Forms no longer need JavaScript handlers. Buttons send POST directly to the server. The server returns an HTML fragment. That fragment replaces part of the page. That's it.

But the simplicity is more than syntax. Carson put a book on top of it. Hypermedia Systems — a re-reading of Roy Fielding's REST dissertation that takes HATEOAS seriously and asks "why did we build a JSON API, then a client-side router on top of it, then state management on top of that, then a cache on top of that, then an optimizing compiler on top of all that?" The answer is provocative. "We didn't need to."

This essay maps the HTMX camp — the anti-SPA camp, or the "boring web" renaissance — in 2026. HTMX 2.x core, _hyperscript as Carson's second creation, Datastar as the next-gen candidate, Alpine.js as the compromise position, the conceptual framework from Hypermedia Systems, production cases like 37signals, and — most importantly — where HTMX is wrong.

This is not an HTMX apologetic. The goal is to give you every tool needed to decide when HTMX is right and when React is right in a single breath.


1. The Hypermedia Thesis — Re-reading Roy Fielding

Every HTMX claim begins with one dissertation. In 2000, Roy Fielding's "Architectural Styles and the Design of Network-based Software Architectures" — the document we shorten to REST.

Fielding put six constraints on REST. Client-server, stateless, cacheable, uniform interface, layered system, and — HATEOAS: Hypermedia As The Engine Of Application State.

The last one is the kernel. HATEOAS says "the client must obtain information about the next action only from hypermedia returned by the server." Responses must contain "what you can do next" as links and forms.

Carson's diagnosis:

  • 99% of what we call REST today follows the other five constraints and skips HATEOAS.
  • JSON APIs give data but not actions. So clients must encode "what is possible next" in their own code.
  • That makes the client a duplicate of the domain model, forcing two-way synchronization.
  • HTML, by contrast, is hypermedia. The form tag contains action. The a tag contains state transition.

HTMX's thesis is then simple.

"Browsers are already excellent hypermedia clients. HTMX extends that client's vocabulary."

Plain HTML triggers server requests only on link clicks and form submits. HTMX extends that.

  • Any event can be a trigger (hx-trigger)
  • Any HTTP verb is available (hx-get, hx-post, hx-put, hx-patch, hx-delete)
  • Any part of the page can be the swap target (hx-target, hx-swap)

That is all of HTMX. One line summary — "any element, any event, any HTTP verb, any swap target."


2. HTMX 2.x Core — Four Attributes That Cover 99%

HTMX 2.0 went GA in January 2024. 2.1 shipped in 2025, 2.2 in early 2026. Current stable is 11.4 KB gzipped from htmx.org. Zero dependencies. IE11 support remains on the 1.x line; 2.x targets evergreen browsers only.

2.1 Installation — One Script Tag

<script src="https://unpkg.com/htmx.org@2.0.4"></script>

That's it. An npm package exists, but HTMX explicitly recommends the script tag. No build step, no bundler, no tree-shaking anxiety.

2.2 First Example — A Search Form

In a traditional SPA, "live search results that update as you type" requires — useState, useDeferredValue, debounce hooks, cache invalidation, abort controllers, a loading state machine — at minimum 50 lines of React.

HTMX version:

<input type="search" name="q"
       hx-get="/search"
       hx-trigger="input changed delay:300ms, search"
       hx-target="#results"
       hx-indicator=".spinner" />

<div id="results"></div>
<div class="spinner htmx-indicator">Searching...</div>

Reading what happens line by line.

  • hx-get="/search" — On trigger, fire GET /search?q=.... The name="q" attribute becomes the query parameter automatically.
  • hx-trigger="input changed delay:300ms, search" — Trigger on input events when the value changes and 300ms passes without another event. Or when the user hits the search key.
  • hx-target="#results" — Replace contents of #results with the response.
  • hx-indicator=".spinner" — While the request is in flight, add htmx-request class to .spinner. Show/hide via CSS.

The server? Express, FastAPI, Django, Rails, Phoenix, Spring — anything that can return HTML fragments.

@app.get("/search")
def search(q: str):
    results = db.search(q)
    return render_template("results_fragment.html", results=results)

What this pattern means is deep. Loading state, debounce, abort, race condition, caching — all handled by HTMX. You write only business logic.

2.3 The 13 Core Attributes

HTMX's surface area is small. Thirteen core attributes express almost everything.

AttributeRole
hx-get / hx-post / hx-put / hx-patch / hx-deleteFire HTTP request
hx-triggerWhat event triggers it (click, input, every 2s, revealed, ...)
hx-targetWhich element to apply the response to (CSS selector)
hx-swapHow to apply it (innerHTML, outerHTML, beforebegin, afterend, ...)
hx-valsExtra values to send (JSON or JS function)
hx-headersCustom headers
hx-indicatorIndicator during request
hx-confirmconfirm() dialog before sending
hx-push-urlUpdate browser URL on response
hx-boostAuto-upgrade plain links/forms to AJAX

hx-boost alone is interesting. Add <body hx-boost="true"> and every a and form on the page automatically becomes AJAX, swapping only the body portion of responses. Turbo and Inertia-like effects in one attribute.

2.4 The Power of hx-swap — Declarative DOM Mutation

hx-swap decides how the response HTML is applied to the DOM.

  • innerHTML (default): replace target's contents
  • outerHTML: replace the target itself
  • beforebegin / afterbegin / beforeend / afterend: insert at adjacent positions
  • delete: remove the target (ignore response)
  • none: no DOM change (side effects only)

Add modifiers like hx-swap="outerHTML swap:1s settle:1s scroll:#top" and you get — 1 second wait before swap (animation), 1 second settle (apply transition classes), scroll to #top when done — all in one line.

2.5 The Power of hx-trigger — An Event DSL

hx-trigger is a small DSL.

<!-- Auto-refresh every 5 seconds -->
<div hx-get="/notifications" hx-trigger="every 5s"></div>

<!-- Trigger once when entering viewport (lazy load) -->
<div hx-get="/heavy-section" hx-trigger="revealed"></div>

<!-- Trigger on another element's event -->
<div hx-get="/related" hx-trigger="updated from:#article"></div>

<!-- Keyboard shortcuts -->
<form hx-post="/save" hx-trigger="keyup[ctrlKey&&key=='s'] from:body"></form>

Polling becomes one line with every Ns. Infinite scroll becomes one line with revealed. from:#selector listens to events on other elements. In domains where polling is enough — notifications, stock tickers, live counters — HTMX answers immediately without WebSockets.


3. Hyperscript — Carson's Second Provocation

HTMX handles things that can use a server round-trip. But pure client-side actions like "toggle the class of the div next to this button when clicked" are also common. Bringing in React is overkill. jQuery feels dated.

Carson placed _hyperscript (underscore-prefixed, shortened to _hs) into that gap. An event-handler DSL that reads like English sentences.

<button _="on click toggle .active on #panel">Toggle</button>

Read it as a sentence. "On click, toggle the class .active on #panel." Plain English. The parser accepts it as-is. The inspiration is HyperTalk (the HyperCard scripting language).

3.1 A More Complex Example

<input _="on input
            if my value's length < 3
              hide #suggestions
            else
              show #suggestions
              put 'Searching...' into #suggestions
            end" />

What is this? If input length is under 3, hide #suggestions. Otherwise show it and fill it with "Searching...". The code reads as English.

3.2 fetch in One Line, Async

<button _="on click
            fetch /api/heavy
            put it into #result">
  Load
</button>

After fetch /api/heavy, it on the next line refers to the response. put it into #result — drop the response into #result. No JavaScript Promise or async/await dissonance.

3.3 Common Patterns

  • Toggle class: on click toggle .open on .menu
  • Confirmation: on click if confirm('Sure?') trigger submit on me
  • Timer: on load wait 3s then add .visible to #toast
  • Drag: on mousedown set $dragging to me on mousemove if $dragging then ...
  • Event delegation: on click from .row in me ...

3.4 The Reality of Hyperscript Adoption — Honest in 2026

Let us be honest. Hyperscript has not been adopted nearly as widely as HTMX. GitHub stars sit around 1/10 of HTMX's. The reasons are clear.

  • VS Code syntax highlighting is weak — strings within quotes do not get colored.
  • Error messages are thin — being English sentences makes errors hard to localize.
  • No TypeScript integration — no type safety.
  • Alpine is more familiar — JavaScript-style expressions feel native to React/Vue veterans.

So most HTMX users pair HTMX with Alpine.js instead of Hyperscript. Which brings us to the next chapter.


4. Alpine.js — A Bridge for Those Who Want Reactivity

Built by Caleb Porzio, Alpine.js fuses "Vue 3's reactivity, jQuery's directness, Tailwind's inline approach." 15 KB. Zero dependencies. Established itself as the natural companion to HTMX.

4.1 Basics

<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open" x-transition>Hello!</div>
</div>
  • x-data — opens a component scope and defines a reactive state object
  • @click (or x-on:click) — event handler
  • x-show — conditional rendering (display toggle)
  • x-transition — automatic transitions

Five directives express 90%. The rest is x-text, x-html, x-bind, x-model, x-for, x-if.

4.2 HTMX + Alpine — The Golden Combo

HTMX for server round-trips. Alpine for local UI state. They do not conflict and merge naturally.

<div x-data="{ mode: 'list' }">
  <button @click="mode = 'list'" :class="{ 'active': mode === 'list' }">List</button>
  <button @click="mode = 'grid'" :class="{ 'active': mode === 'grid' }">Grid</button>

  <div hx-get="/items" hx-trigger="load" hx-target="this">
    <!-- Server-rendered HTML lands here -->
  </div>
</div>

View-mode toggle (client state) goes to Alpine. Item loading (server state) goes to HTMX. Each does what it is good at.

4.3 alpine-ajax — The Other Side of the Coin

Interestingly, the Alpine camp has a third-party plugin called alpine-ajax. It does HTMX-like server communication on top of Alpine. The existence of this tool is itself a message. The HTMX thesis and the Alpine thesis see the same thing from different entry points.


5. Datastar — The Next-Gen Hypermedia Framework

Delaney Gillilan's Datastar is a provocative attempt to merge HTMX + Alpine into one library. 1.0 shipped in 2024. As of 2026, 1.3 is stable. ~10 KB.

5.1 The Key Difference — SSE First

HTMX receives ordinary HTTP responses and swaps the DOM. Datastar treats Server-Sent Events (SSE) as a first-class citizen. A single request receives multiple fragment updates as a stream.

<button data-on-click="@get('/calculate')">
  Calculate
</button>

<div id="step1"></div>
<div id="step2"></div>
<div id="final"></div>

The server takes a single request and responds via SSE.

event: datastar-fragment
data: selector #step1
data: fragments <div id="step1">Step 1 done</div>

event: datastar-fragment
data: selector #step2
data: fragments <div id="step2">Step 2 done</div>

event: datastar-fragment
data: selector #final
data: fragments <div id="final">Result: 42</div>

LLM streaming, progress display, live dashboards — that is Datastar's home turf. HTMX can do similar things via the htmx-ext-sse extension, but Datastar was designed with SSE assumed from the start.

5.2 Expressions — Alpine Style

<input data-bind-name />
<div data-text="$name"></div>
<button data-on-click="$count++">+</button>
<p data-text="$count"></p>

data-bind-*, data-text, data-on-* — almost compatible vocabulary with Alpine. Signal-based reactivity. All state is accessed via $.

5.3 Datastar vs HTMX — How to Choose

  • LLM streaming/real-time dashboards → Datastar (SSE first-class)
  • Traditional CRUD/form-centric → HTMX (mature ecosystem, better compatibility)
  • Non-trivial client-side state → Datastar (Alpine features built in)
  • Rich server-side library integration → HTMX (helpers exist for Django, Rails, FastAPI, Express)

In 2026 HTMX still has the overwhelming majority. Datastar's "SSE-native" strength entered with the LLM era, but adoption is still early-adopter.


6. Hypermedia Systems — A Book That Built a Worldview

Written by Carson Gross, Adam Stepinski, and Deniz Akşimşek, Hypermedia Systems shipped in 2023 and is open-access on GitHub. The subtitle is "HTML, HTTP, and the Hypermedia Architecture."

6.1 Questions the Book Answers

  • Why did we accept the JSON API + JavaScript client pattern as the default?
  • Is HATEOAS actually practical, or only academic curiosity?
  • Can hypermedia work on mobile? (The book also covers Hyperview, an Android/iOS hypermedia client.)
  • How does a hypermedia-built system scale?

6.2 The LoB Principle — Locality of Behavior

A core principle the book introduces is Locality of Behavior (LoB). One sentence — "An element's behavior should be understandable by looking at the element."

React separates behavior into handlers somewhere in the component tree. CSS goes into external files. JS event delegation also pulls behavior elsewhere. We were taught this is good (Separation of Concerns), but Carson proposes a different principle. Not separated concerns, but co-located behavior.

<!-- Behavior visible in one place -->
<button hx-post="/like"
        hx-target="#count"
        _="on click toggle .liked on me">
  Like
</button>

What this button does — POST request, count update, class toggle — you do not need to open another file. Designers, backend developers, and AI assistants alike understand this single line.

6.3 SoC vs LoB — Which Is Right?

The answer is both are right, context decides. In large React apps, component abstractions compensate for absent LoB. In HTMX apps, screens are small and LoB works directly. For an internal tool with 5 users, follow LoB. For a SaaS aiming at 100M users, component abstractions may be required.

The point — LoB should be the default, and abstraction should be justified.


7. Production Cases — Who Actually Uses This

HTMX is not a toy. In 2026 these cases are public.

  • GitHub — parts of the new PR page and issue comments. Mostly Turbo-driven but HTMX-inspired patterns are common.
  • NASA JPL — many internal tools on HTMX (public conference presentations).
  • Contexte — a French political news site, fully redesigned on HTMX.
  • Replicant.au — an Australian indie SaaS, full HTMX + Hyperscript.
  • David Heinemeier Hansson and 37signals — Hotwire (Turbo + Stimulus) is their main stack, but as the loudest voice in "frontend simplification" they share the HTMX camp's worldview.
  • Bunny.net — CDN company, admin dashboard on HTMX.
  • Quanta Magazine — some interactive content.

DHH (David Heinemeier Hansson) is not an HTMX user, but he is the biggest voice in the "complexity is not fashion" campaign. His Hotwire is the Rails camp's HTMX, philosophically nearly identical. His 2024-2025 series of talks — "The One Person Framework", "What is Modern Web Development?" — are widely cited in HTMX circles.

7.1 How Big Can a Company Get?

Small SaaS, internal tools, content sites, admin panels, CMSes — HTMX is clearly strong here. PWAs that must behave like mobile apps, collaboration tools where offline is a first-class citizen, client-heavy games that mount a council of WebGL canvases — HTMX is weak there.

37signals' Basecamp sits somewhere between. It is collaboration software, but heavy client state like real-time sync is light.


8. Honest Weaknesses — Where HTMX Is Wrong

I said this was not an apologetic. HTMX is weak or wrong in the following areas.

8.1 Rich Client-Side State

  • Figma-style collaborative whiteboards — many object states must be tracked on the client. Server round-trips are unrealistic.
  • Notion-style block editors — going to the server on every keystroke is impossible.
  • VS Code Web — editor state, syntax trees, LSP responses — all must be handled on the client.

For these, React/Vue/Solid are right.

8.2 Offline-First

PWAs, mobile-first, apps that must work on airplanes. HTMX assumes a server response, so offline breaks. Service Worker + IndexedDB + sync queue — that combination is SPA territory.

8.3 Heavy Client-Side Animation

  • 60fps canvas drawing
  • Physics simulation
  • 3D interaction
  • Timeline-based video editing

HTMX has little to offer here.

8.4 Server Load

HTMX hits the server on every interaction. The traffic pattern shifts from static asset-centric to dynamic HTML generation-centric. Server-side caching, edge rendering, CDN strategy become mandatory. SPAs offloaded some of that to CDN and client; HTMX servers must reabsorb it.

8.5 Mobile Networks

Response times over 200ms collapse the user experience. Optimistic update patterns are awkward; you risk the "I clicked but nothing happened" impression.

Mitigations exist — hx-disabled-elt, hx-indicator, local Alpine state for instant UI updates confirmed later by the server response. But this needs more care than an SPA that is "fast by default."


9. Decision Framework — When HTMX

Use the table below as a baseline. Sum the column scores. Positive total → HTMX. Negative → SPA.

QuestionHTMX scoreSPA score
Is it form-centric?+20
Is it content-centric?+20
Is it an internal tool/admin?+20
Is the team 5 or fewer?+10
Is the server language strong (Django/Rails/Phoenix)?+20
Does SEO matter?+1-1
Is offline required?-2+2
Is there a lot of client-side state?-2+2
Are 60fps animations core?-2+2
Do you need to share code with a mobile app?-1+2
Over 50K concurrent users?-1+1
Designers work HTML/CSS first?+10
Existing codebase is React?-2+1

The scores are not absolute. But they form a starting point. Near zero — you can mix the patterns. Build large regions in HTMX and isolate true SPA islands (editor, whiteboard) in React. This hybrid is increasingly common.

9.1 Migration Strategy

Existing React app, want to move to HTMX — total rewrite is almost always wrong. Use the Strangler Fig pattern.

  1. Write new pages in HTMX — new admin pages, new modules
  2. Target-migrate simple CRUD screens — form pages, list pages
  3. Keep client-heavy screens in React — isolate as islands (editor, dashboard)
  4. Shared header/navigation via server-side rendering — accelerate transitions with hx-boost

10. The HTMX Camp on One Page

10.1 HTMX — Inline-Edit Form

<!-- Display mode -->
<div hx-target="this" hx-swap="outerHTML">
  <p>Title: Hello, World</p>
  <button hx-get="/posts/1/edit">Edit</button>
</div>

<!-- Server response: edit mode -->
<form hx-put="/posts/1" hx-target="this" hx-swap="outerHTML">
  <input name="title" value="Hello, World" />
  <button type="submit">Save</button>
  <button hx-get="/posts/1">Cancel</button>
</form>

Server-side pseudocode (Python/FastAPI):

@app.get("/posts/{id}/edit")
def edit_form(id: int):
    post = db.get_post(id)
    return templates.TemplateResponse("post_edit.html", {"post": post})

@app.put("/posts/{id}")
def update_post(id: int, title: str = Form(...)):
    db.update_post(id, title=title)
    post = db.get_post(id)
    return templates.TemplateResponse("post_display.html", {"post": post})

Mode transition logic, state machines, form libraries — all gone.

10.2 Hyperscript — Closing a Modal in One Line

<div id="modal" class="hidden">
  <div class="overlay" _="on click hide #modal"></div>
  <div class="content">
    <button _="on click hide #modal">Close</button>
    <p>Esc also closes.</p>
  </div>
</div>

<script>
  document.addEventListener('keydown', e => {
    if (e.key === 'Escape') document.getElementById('modal').classList.add('hidden')
  })
</script>

That JavaScript can also be one Hyperscript line.

<body _="on keydown[key=='Escape'] from window hide #modal">

10.3 Datastar — LLM Streaming

<button data-on-click="@post('/chat', { messages: $messages })">
  Send
</button>

<div id="response"></div>

The server sends token-level fragments via SSE.

event: datastar-fragment
data: merge inner
data: selector #response
data: fragments Hello

event: datastar-fragment
data: merge inner
data: selector #response
data: fragments Hello,

event: datastar-fragment
data: merge inner
data: selector #response
data: fragments Hello, world!

Datastar assumes this use case from the start. HTMX can do similar via the htmx-ext-sse extension, but it is not first-class.


Epilogue — The Renaissance of the Boring Web

The first decade of the web was simple. There was HTML, forms, links, and servers. Then jQuery came, Ajax came, Backbone and Angular came, and React arrived — and everything we built suddenly had to be rendered twice. Once on the server, once on the client. Once in Python, once in JavaScript. And a larger and larger fraction of our energy went into reconciling the two.

Carson Gross stopped at some point and asked. "Did we really need all this?"

The answer — sometimes yes, sometimes no. HTMX is not right everywhere. But half the places we used React, React was not right either. And half of that half — admins, CRUD, internal tools, content sites — one line of HTMX was enough.

The reason the HTMX camp is gaining strength in 2026 is simple. In the AI era, simplicity becomes a more valuable resource. LLMs are weak at tracking state flow through a React component tree, but they instantly understand <button hx-post="/like">. Codebases that collaborate with AI assistants benefit disproportionately from LoB.

The boring web is not over. It was just briefly forgotten.

Checklist — Before Starting Your Next Project

  • Is it form-centric or data-centric? Form-centric → start with HTMX
  • Strong integration with a server language? Django/Rails/Phoenix → HTMX is natural
  • Is offline a core requirement? → SPA or PWA
  • Are 60fps canvas/3D the core? → SPA
  • Do designers work directly in HTML/CSS? → HTMX is familiar to them
  • Team size 5 or fewer? → simplicity ROI is highest
  • Are you pair-programming with AI assistants? → LoB code is better understood

Anti-Patterns — Do Not

  • HTMX as SPA mimicry — do not handle every page transition with hx-boost and a client router. Keep server-side routing so URLs work consistently
  • Server round-trip for every interaction — pure UI actions like class toggles belong in Hyperscript/Alpine. Do not call the server where it is not needed
  • Giant HTML fragments — response fragments must be small. If you replace half the page on every update, SoC is breaking down
  • Hyperscript AND Alpine together — they cover similar territory. Pick one
  • No tests — HTMX hits the server more often. Integration tests (Playwright-style) become more important
  • JSON API design that ignores hypermedia — designing a REST API as JSON and then layering HTMX pages on top equals the two-way-sync SPA pattern. Make hypermedia the first-class response
  • Starting without knowing the extensions — learn htmx-ext-sse, htmx-ext-ws, htmx-ext-response-targets. The small core requires extensions often

Next Post Preview

The next post covers a practical layout for the HTMX + Django/FastAPI/Rails backend stack — template fragment management strategies, auth flow, file upload, form validation, CSRF, and "HTMX-friendly controller patterns." After that, hybrid patterns mixing HTMX and React islands on one page — a Notion-style editor mounted as React with HTMX wrapping around it.


References