--- id: frontend-htmx-hotwire title: HTMX / Hotwire — Server-driven UI category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, htmx, hotwire, vibe-coding] tech_stack: { language: "HTML / Server", applicable_to: ["Frontend"] } applied_in: [] aliases: [HTMX, Hotwire, Turbo, Stimulus, server-driven UI, MPA renaissance, Phoenix LiveView] --- # HTMX / Hotwire / Phoenix LiveView > SPA 의 반발. **Server 가 HTML 보냄, JS 최소**. HTMX (any backend), Hotwire (Rails), Phoenix LiveView (Elixir). 작은 bundle + 빠른 dev. ## 📖 핵심 개념 - HTML over the wire: server → HTML fragment. - AJAX without JS: HTMX attribute. - Stateful server: WebSocket 으로 push. - Less JS: 인터랙션 만 client. ## 💻 코드 패턴 ### HTMX 기본 ```html 42 ``` → JS 0. Server 가 HTML 반환 → DOM swap. ### Triggers ```html
Loading...
...
``` ### Swap modes ```html ``` ### Boost (link / form 자동 AJAX) ```html About
...
``` → MPA 처럼 link / form. SPA-like UX. ### Indicator ```html Saving... ``` → Loading state 자동. ### OOB swap (다른 곳도 update) ```html
Updated
Save successful!
``` → 한 응답 가 여러 곳 update. ### Confirm ```html ``` ### Server (any backend) ```ts // Hono app.post('/like', async (c) => { const postId = c.req.param('postId'); const newCount = await incrementLikes(postId); return c.html(`${newCount}`); }); app.post('/users', async (c) => { const formData = await c.req.formData(); const user = await createUser(Object.fromEntries(formData)); return c.html(`
  • ${user.name} (${user.email})
  • `); }); ``` → HTML fragment 반환. ### Hyperscript (HTMX 의 sister) ```html
    Content
    ``` → JS-like inline language. 작은 인터랙션. ### Hotwire Turbo (Rails) ```html Add ``` ```html ``` ### Hotwire Stimulus ```html
    ``` ```js // hello_controller.js import { Controller } from '@hotwired/stimulus'; export default class extends Controller { static targets = ['name', 'output']; greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`; } } ``` ### Phoenix LiveView (Elixir) ```elixir defmodule MyAppWeb.UserLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do {:ok, assign(socket, users: list_users(), query: "")} end def handle_event("search", %{"q" => q}, socket) do users = search_users(q) {:noreply, assign(socket, users: users, query: q)} end def render(assigns) do ~H""" """ end end ``` → WebSocket + diff push. SPA UX + server logic. ### Use cases ``` HTMX: ✅ CRUD apps ✅ Admin dashboards ✅ Forms / wizards ✅ E-commerce (product list) ✅ Real-time updates (polling) ❌ Heavy interactive (game, drawing) ❌ Offline-first ❌ Mobile native ``` ### When 가치 ``` - Backend team 가 frontend 도 — JS 적게 - 빠른 dev cycle - 작은 / medium app - SEO critical - Mobile slow network - Server-side state important ``` ### When NOT 가치 ``` - Heavy client interaction (drag, drawing) - Offline app - Mobile native (RN / Flutter) - 큰 / 복잡 UI state ``` ### Bundle ``` HTMX: ~14 KB (gzip) Hotwire: ~50 KB React: ~45 KB + app code → HTMX = 가장 작음. ``` ### Performance ``` HTMX: - Server-side render — fast TTFB - 작은 JS — 빠른 hydration X (no hydration) - AJAX = 작은 response → Marketing site / blog / CRUD = 매우 빠름. ``` ### Real-time (HTMX SSE) ```html
    Waiting for messages...
    ``` ```ts // Server app.get('/events', (c) => { return new Response( new ReadableStream({ start(controller) { const send = (data: any) => { controller.enqueue(`data:
    ${data}
    \n\n`); }; // ... }, }), { headers: { 'Content-Type': 'text/event-stream' } } ); }); ``` ### React + HTMX hybrid ``` 일부 page = HTMX (form, CRUD). 일부 page = React (interactive). 같은 app. ``` ### Test ```ts // Playwright test('like button increments', async ({ page }) => { await page.goto('/post/1'); const button = page.getByText('Like'); const counter = page.locator('#likes'); await expect(counter).toHaveText('42'); await button.click(); await expect(counter).toHaveText('43'); }); ``` → E2E 가 자연 (HTML server). ### Pitfalls ``` 1. Backend = template (Handlebars / EJS / 자체). 2. CSRF token 매 form. 3. Validation = server-side. 4. URL state — 명시적 hx-push-url. 5. Browser back button — 자동 X. Configure. ``` ### Datastar (modern alternative) ```html
    ``` → HTMX 의 modern 후속. SignalR-like reactivity. ### Build / deploy ``` Backend = template + routes. Frontend = HTML + 작은 JS (HTMX). Deploy = 일반 server. → Vercel / Netlify (static) X — server 필요. ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | CRUD admin | HTMX | | Rails app | Hotwire (built-in) | | Phoenix / Elixir | LiveView | | 작은 인터랙션 | HTMX + Stimulus / Hyperscript | | Heavy SPA | React / Solid | | Backend-heavy team | HTMX | ## ❌ 안티패턴 - **HTMX + 큰 client state**: 잘못된 선택. SPA. - **Server template 없음**: HTML fragment 어떻게? - **CSRF 없음**: form 위험. - **모든 page 가 sse-connect**: server 부담. - **Validation client only**: server 가 진실. - **JS 부족 — 사용자 못 input**: progressive 검토. ## 🤖 LLM 활용 힌트 - HTMX = MPA renaissance. - Server-side template + HTML fragment. - Boost = SPA-like links / forms. - 작은 / CRUD app 의 sweet spot. ## 🔗 관련 문서 - [[Frontend_Progressive_Enhancement]] - [[Backend_Server_Components_Pattern]] - [[Backend_Hono_Modern]]