htmx 완전 가이드 | JavaScript 없이 동적 웹 만들기
이 글의 핵심
React나 Vue 없이 HTML 속성만으로 동적 웹을 만드는 htmx. AJAX, WebSocket, Server-Sent Events를 HTML 태그에서 직접 제어하며, JavaScript를 최소화해 단순하고 빠른 웹 개발을 가능하게 합니다.
이 글의 핵심
htmx는 JavaScript 없이 HTML 속성만으로 동적 웹을 만드는 혁신적인 라이브러리입니다. React/Vue 같은 복잡한 프레임워크 없이도 AJAX·WebSocket·Server-Sent Events를 HTML 태그에서 직접 제어할 수 있습니다.
목차
htmx란?
htmx는 2020년 Carson Gross가 개발한 하이퍼미디어 기반 웹 라이브러리입니다.
🚀 핵심 철학
“HTML을 다시 위대하게 (Make HTML Great Again)”
- HTML이 모든 HTTP 메서드를 사용할 수 있어야 한다
- 모든 HTML 요소가 AJAX 요청을 보낼 수 있어야 한다
- 서버가 HTML을 응답하면 클라이언트가 교체한다
💡 왜 htmx인가?
Before (React/Vue)
// 복잡한 JavaScript 코드
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setData(data));
}, []);
return <div>{data.map(user => <div>{user.name}</div>)}</div>;
After (htmx)
<!-- 단순한 HTML -->
<button hx-get="/users" hx-target="#list">
Load Users
</button>
<div id="list"></div>
htmx 시작하기
설치
CDN 사용 (권장)
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
<!-- htmx 사용 가능 -->
</body>
</html>
npm으로 설치
npm install htmx.org
// app.js
import 'htmx.org';
핵심 개념
1️⃣ hx-get: AJAX GET 요청
<!-- 버튼 클릭 시 /users 요청 후 응답을 #result에 삽입 -->
<button hx-get="/users" hx-target="#result">
Load Users
</button>
<div id="result"></div>
서버 응답 예시 (Node.js/Express)
app.get('/users', (req, res) => {
res.send(`
<ul>
<li>Alice</li>
<li>Bob</li>
<li>Charlie</li>
</ul>
`);
});
2️⃣ hx-post: AJAX POST 요청
<!-- 폼 제출 시 /submit으로 POST 요청 -->
<form hx-post="/submit" hx-target="#message">
<input name="username" type="text" placeholder="Username" />
<button type="submit">Submit</button>
</form>
<div id="message"></div>
서버 응답
app.post('/submit', (req, res) => {
const { username } = req.body;
res.send(`<p>Welcome, ${username}!</p>`);
});
3️⃣ hx-swap: 응답 삽입 방식
<!-- innerHTML (기본값) -->
<button hx-get="/content" hx-target="#box" hx-swap="innerHTML">
Replace Content
</button>
<!-- outerHTML: 타겟 자체를 교체 -->
<button hx-get="/content" hx-target="#box" hx-swap="outerHTML">
Replace Element
</button>
<!-- beforeend: 타겟 내부 끝에 추가 -->
<button hx-get="/content" hx-target="#list" hx-swap="beforeend">
Append
</button>
<!-- afterbegin: 타겟 내부 시작에 추가 -->
<button hx-get="/content" hx-target="#list" hx-swap="afterbegin">
Prepend
</button>
4️⃣ hx-trigger: 이벤트 트리거
<!-- 클릭 시 (기본값) -->
<button hx-get="/data" hx-trigger="click">Click Me</button>
<!-- 입력할 때마다 -->
<input hx-get="/search" hx-trigger="keyup" />
<!-- 입력 후 500ms 지연 (디바운스) -->
<input hx-get="/search" hx-trigger="keyup delay:500ms" />
<!-- 마우스 호버 시 -->
<div hx-get="/info" hx-trigger="mouseenter">Hover Me</div>
<!-- 로드 시 자동 실행 -->
<div hx-get="/initial-data" hx-trigger="load">Loading...</div>
<!-- 폴링 (2초마다 자동 요청) -->
<div hx-get="/live-data" hx-trigger="every 2s">Live Data</div>
5️⃣ hx-target: 응답 삽입 위치
<!-- ID로 타겟 지정 -->
<button hx-get="/data" hx-target="#result">Load</button>
<div id="result"></div>
<!-- 가장 가까운 부모 찾기 -->
<div id="container">
<button hx-get="/data" hx-target="closest div">Load</button>
</div>
<!-- 현재 요소 자체 -->
<button hx-get="/data" hx-target="this">Load</button>
실전 예제
📝 예제 1: 실시간 검색
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
<h1>실시간 검색</h1>
<!-- 입력 시 500ms 후 서버에 요청 -->
<input
type="text"
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#results"
placeholder="검색어 입력..."
/>
<div id="results"></div>
</body>
</html>
서버 (Express)
app.get('/search', (req, res) => {
const query = req.query.q || '';
const results = ['Apple', 'Banana', 'Cherry']
.filter(item => item.toLowerCase().includes(query.toLowerCase()));
const html = results.length > 0
? `<ul>${results.map(r => `<li>${r}</li>`).join('')}</ul>`
: '<p>검색 결과 없음</p>';
res.send(html);
});
📋 예제 2: 무한 스크롤
<div id="posts">
<div class="post">Post 1</div>
<div class="post">Post 2</div>
<div class="post">Post 3</div>
<!-- 뷰포트에 보일 때 자동 로드 -->
<div
hx-get="/posts?page=2"
hx-trigger="revealed"
hx-swap="outerHTML"
>
Loading more...
</div>
</div>
서버
app.get('/posts', (req, res) => {
const page = parseInt(req.query.page) || 1;
const posts = generatePosts(page);
const html = `
${posts.map(p => `<div class="post">${p.title}</div>`).join('')}
<div
hx-get="/posts?page=${page + 1}"
hx-trigger="revealed"
hx-swap="outerHTML"
>
Loading more...
</div>
`;
res.send(html);
});
🗑️ 예제 3: 삭제 버튼
<ul id="todo-list">
<li>
Task 1
<button
hx-delete="/todos/1"
hx-target="closest li"
hx-swap="outerHTML swap:1s"
>
Delete
</button>
</li>
<li>
Task 2
<button
hx-delete="/todos/2"
hx-target="closest li"
hx-swap="outerHTML swap:1s"
>
Delete
</button>
</li>
</ul>
서버
app.delete('/todos/:id', (req, res) => {
const id = req.params.id;
// DB에서 삭제...
// 빈 응답 (타겟이 사라짐)
res.send('');
});
📱 예제 4: 폼 검증
<form hx-post="/register" hx-target="#result">
<input
name="email"
type="email"
required
hx-post="/validate-email"
hx-trigger="blur"
hx-target="#email-error"
/>
<div id="email-error"></div>
<input name="password" type="password" required />
<button type="submit">Register</button>
</form>
<div id="result"></div>
서버
// 이메일 검증
app.post('/validate-email', (req, res) => {
const { email } = req.body;
const exists = checkEmailExists(email);
if (exists) {
res.send('<p style="color:red">이미 사용 중인 이메일입니다.</p>');
} else {
res.send('<p style="color:green">사용 가능한 이메일입니다.</p>');
}
});
// 회원가입
app.post('/register', (req, res) => {
const { email, password } = req.body;
// DB에 저장...
res.send('<p>회원가입 성공!</p>');
});
고급 기능
🔄 hx-boost: 전체 페이지 AJAX화
<!-- 모든 링크와 폼을 자동으로 AJAX로 전환 -->
<body hx-boost="true">
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main>
<!-- 페이지 전환 시 이 부분만 교체됨 -->
</main>
</body>
📡 Server-Sent Events (SSE)
<!-- 서버에서 실시간 업데이트 수신 -->
<div
hx-ext="sse"
sse-connect="/live-updates"
sse-swap="message"
>
Waiting for updates...
</div>
서버 (Node.js)
app.get('/live-updates', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
setInterval(() => {
const data = `<p>Update at ${new Date().toLocaleTimeString()}</p>`;
res.write(`data: ${data}\n\n`);
}, 2000);
});
🔌 WebSocket
<div
hx-ext="ws"
ws-connect="/chat"
>
<form ws-send>
<input name="message" />
<button type="submit">Send</button>
</form>
<div id="messages"></div>
</div>
htmx vs React/Vue
| 기능 | htmx | React/Vue |
|---|---|---|
| 번들 크기 | 14KB | 45KB+ |
| JavaScript | 최소 | 많음 |
| 서버 렌더링 | ✅ 기본 | ⚠️ 복잡 (Next.js/Nuxt) |
| SEO | ✅ 완벽 | ⚠️ SSR 필요 |
| 초기 로딩 | ⚡ 빠름 | 🐢 느림 |
| 복잡한 UI | ⚠️ 제한적 | ✅ 강력 |
| 학습 곡선 | 🟢 쉬움 | 🔴 어려움 |
언제 htmx를 사용할까?
✅ htmx가 적합한 경우
- CRUD 애플리케이션: 게시판, 관리자 패널
- 서버 렌더링 중심: Django, Rails, Laravel, Express
- 단순한 동적 기능: 폼 제출, 페이지 부분 업데이트
- SEO 중요: 콘텐츠 중심 사이트
- 빠른 개발: 프로토타입, MVP
❌ htmx가 부적합한 경우
- 복잡한 클라이언트 상태: 실시간 협업 도구
- 오프라인 우선: PWA, 로컬 캐싱
- 고도로 인터랙티브한 UI: 그래프 편집기, 게임
- 대규모 SPA: Gmail, Figma 같은 앱
핵심 정리
✅ htmx의 장점
- 단순함: HTML 속성만으로 동적 웹 구현
- 가벼움: 14KB로 React보다 3배 작음
- 빠른 초기 로딩: 서버 렌더링으로 즉시 표시
- SEO 친화적: HTML 기반으로 검색 엔진 최적화
- 점진적 향상: 기존 사이트에 쉽게 추가 가능
🚀 다음 단계
- htmx 공식 문서에서 심화 학습
- htmx GitHub에서 소스 코드 탐색
- htmx Discord에서 커뮤니티 참여
시작하기: HTML에
<script src="https://unpkg.com/[email protected]"></script>한 줄 추가하고, JavaScript 없는 동적 웹을 경험하세요! 🚀