본문으로 건너뛰기
Previous
Next
htmx 완전 가이드 | JavaScript 없이 동적 웹 만들기

htmx 완전 가이드 | JavaScript 없이 동적 웹 만들기

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

기능htmxReact/Vue
번들 크기14KB45KB+
JavaScript최소많음
서버 렌더링✅ 기본⚠️ 복잡 (Next.js/Nuxt)
SEO✅ 완벽⚠️ SSR 필요
초기 로딩⚡ 빠름🐢 느림
복잡한 UI⚠️ 제한적✅ 강력
학습 곡선🟢 쉬움🔴 어려움

언제 htmx를 사용할까?

htmx가 적합한 경우

  1. CRUD 애플리케이션: 게시판, 관리자 패널
  2. 서버 렌더링 중심: Django, Rails, Laravel, Express
  3. 단순한 동적 기능: 폼 제출, 페이지 부분 업데이트
  4. SEO 중요: 콘텐츠 중심 사이트
  5. 빠른 개발: 프로토타입, MVP

htmx가 부적합한 경우

  1. 복잡한 클라이언트 상태: 실시간 협업 도구
  2. 오프라인 우선: PWA, 로컬 캐싱
  3. 고도로 인터랙티브한 UI: 그래프 편집기, 게임
  4. 대규모 SPA: Gmail, Figma 같은 앱

핵심 정리

htmx의 장점

  1. 단순함: HTML 속성만으로 동적 웹 구현
  2. 가벼움: 14KB로 React보다 3배 작음
  3. 빠른 초기 로딩: 서버 렌더링으로 즉시 표시
  4. SEO 친화적: HTML 기반으로 검색 엔진 최적화
  5. 점진적 향상: 기존 사이트에 쉽게 추가 가능

🚀 다음 단계


시작하기: HTML에 <script src="https://unpkg.com/[email protected]"></script> 한 줄 추가하고, JavaScript 없는 동적 웹을 경험하세요! 🚀