htmx in 2026: React가 필요 없는 순간 (그리고 반드시 필요한 순간)
프론트엔드 커뮤니티에서 htmx vs React 논쟁이 벌써 2년째 이어지고 있는데, 대부분의 주장이 핵심을 빗나가고 있습니다. 기술 자체가 나빠서가 아니라, 둘을 같은 문제를 푸는 경쟁 관계로 놓고 보기 때문이에요. 그게 아닌데 말이죠.
htmx가 GitHub 스타 47,000개를 넘겼습니다. React는 여전히 수백만 프로덕션 앱을 돌리고 있고요. 2025 State of JS 서베이(2026년 2월 발표)에서 htmx는 써본 개발자들 사이에서 "가장 호감도 높은 기술"을 유지했고, React는 사용률 83.6%에도 불구하고 만족도가 최저치를 기록했어요. 뭔가 바뀌고 있는 건 맞는데, 정확히 뭐가 바뀌고 있는 걸까요?
이건 "htmx 좋고, React 나쁘다" 류의 글이 아닙니다. 의사결정 프레임워크예요. 이 글을 다 읽으면 htmx를 써야 하는 상황, React가 여전히 대체 불가인 상황, 그리고 둘 다 쓰는 게 정답인 상황을 명확히 구분할 수 있을 겁니다.
핵심 아키텍처 차이
기능 비교 전에 근본적인 차이부터 짚어야 합니다. htmx랑 React는 API가 다른 정도가 아니에요. 애플리케이션 상태가 어디에 있어야 하느냐에 대한 철학 자체가 다릅니다.
React: 클라이언트 사이드 애플리케이션 모델
React는 브라우저를 애플리케이션 런타임으로 봅니다. 서버는 JSON을 돌려주는 API고, 클라이언트가 상태 관리, 라우팅, UI 렌더링, 사이드 이펙트 처리를 전부 담당하는 구조예요.
[서버] → JSON → [React 앱] → DOM
↑
상태 관리
라우팅
사이드 이펙트
에러 바운더리
이 모델은 강력합니다. 오프라인 우선 앱, 옵티미스틱 업데이트, 복잡한 애니메이션, 실시간 협업, 리치 인터랙션을 가능하게 해주죠. 근데 그만큼 복잡해요. 2026년 기준 전형적인 React 앱의 구성을 보면:
- 메타 프레임워크 (Next.js, Remix 등)
- 상태 관리 (React Context, Zustand, Jotai, 또는 Server Components)
- 데이터 페칭 (TanStack Query, SWR, 또는 프레임워크 네이티브 로더)
- 빌드 파이프라인 (Vite, Turbopack, Webpack)
- TypeScript 설정
- 테스트 인프라 (Vitest, Playwright, Testing Library)
비즈니스 로직 한 줄 안 쓰고 툴링만 6겹. 이게 2026년 React의 현실이에요.
htmx: 하이퍼미디어 모델
htmx는 브라우저를 문서 뷰어로 봅니다. 다만, 문서의 일부를 교체할 수 있는 뷰어. 서버가 HTML 조각을 내려주고, 클라이언트에는 애플리케이션 상태도, 라우팅 로직도, 빌드 스텝도 없어요.
[서버] → HTML → [브라우저 + htmx] → DOM (교체 대상)
서버가 곧 애플리케이션이고, 브라우저는 렌더러, htmx는 페이지 리로드 없이 일부를 갱신해주는 다리 역할입니다.
<!-- 페이지 리로드 없이 결과를 업데이트하는 검색 폼 --> <input type="search" name="q" hx-get="/search" hx-trigger="input changed delay:300ms" hx-target="#results"> <div id="results"> <!-- 서버가 여기에 HTML 조각을 내려줌 --> </div>
이게 전부예요. JavaScript 파일 없음. 빌드 스텝 없음. 상태 관리 없음. 서버가 검색 쿼리를 처리하고 결과를 HTML로 렌더링하면, htmx가 #results div를 교체합니다. 인터랙션 전체가 HTML 속성으로 정의돼요.
htmx가 이기는 영역: 웹 앱의 80%
React 지지자들이 듣기 싫어하는 불편한 진실이 있어요. 대부분의 웹 앱은 폼, 테이블, 필터, 네비게이션으로 구성된 CRUD 인터페이스입니다. 클라이언트 사이드 애플리케이션 런타임이 필요 없어요. 서버가 HTML을 렌더하고 페이지 일부를 리로드 없이 갱신해주면 끝.
1. 어드민 대시보드와 내부 툴
htmx가 압도적으로 유리한 최대 카테고리. 어드민 패널은 본질적으로 이런 것들의 모음이거든요:
- 정렬, 필터, 페이지네이션이 있는 테이블
- 레코드 생성/수정 폼
- 확인용 모달 다이얼로그
- 피드백용 토스트 알림
htmx에선 각각이 서버 왕복 한 번이면 끝나요. HTML 조각 받아서 swap하면 됩니다:
<!-- 정렬 가능한 테이블 헤더 --> <th hx-get="/users?sort=name&dir=asc" hx-target="#user-table-body" hx-indicator="#loading"> 이름 ↕ </th> <!-- 확인 후 삭제 --> <button hx-delete="/users/42" hx-confirm="이 사용자를 삭제하시겠습니까?" hx-target="closest tr" hx-swap="outerHTML swap:500ms"> 삭제 </button> <!-- 인라인 편집 --> <td hx-get="/users/42/edit-name" hx-trigger="dblclick" hx-swap="innerHTML"> 홍길동 </td>
같은 기능을 React로 구현하면:
// 정렬 상태 관리 const [sortConfig, setSortConfig] = useState({ key: 'name', dir: 'asc' }); // TanStack Query로 데이터 페칭 const { data, isLoading, refetch } = useQuery({ queryKey: ['users', sortConfig, filters, page], queryFn: () => fetchUsers({ ...sortConfig, ...filters, page }), }); // 옵티미스틱 업데이트가 포함된 삭제 뮤테이션 const deleteMutation = useMutation({ mutationFn: deleteUser, onMutate: async (userId) => { await queryClient.cancelQueries(['users']); const previous = queryClient.getQueryData(['users']); queryClient.setQueryData(['users'], (old) => old.filter(u => u.id !== userId) ); return { previous }; }, onError: (err, userId, context) => { queryClient.setQueryData(['users'], context.previous); }, onSettled: () => queryClient.invalidateQueries(['users']), }); // 확인 다이얼로그 상태 const [deleteTarget, setDeleteTarget] = useState(null); const [showConfirm, setShowConfirm] = useState(false);
htmx 버전은 HTML 속성 8줄. React 버전은 JSX 쓰기도 전에 JavaScript 25줄 이상. 거기다 React 쪽이 터지는 포인트도 더 많아요. 스테일 캐시, 옵티미스틱 롤백 버그, 쿼리 간 레이스 컨디션, 구독 쪽 메모리 릭까지.
2. 멀티 스텝 폼과 위저드
htmx는 부분 폼 밸리데이션과 멀티 스텝 플로우를 자연스럽게 처리합니다. 서버가 상태를 제어하니까요:
<!-- 1단계: 서버가 검증 후 2단계를 반환 --> <form hx-post="/onboarding/step-1" hx-target="#wizard-container" hx-swap="innerHTML"> <input name="email" type="email" required> <input name="company" required> <button type="submit">다음</button> </form>
서버가 1단계 검증하고, 세션에 저장하고, 2단계 HTML 내려주면 끝이에요. 클라이언트 폼 상태 관리? 없음. Formik? 없음. React Hook Form? 없음. Zod 스키마를 컴포넌트에 꿰매는 작업? 당연히 없음.
3. 동적 섹션이 있는 콘텐츠 중심 사이트
블로그, 문서 사이트, 이커머스 상품 페이지처럼 핵심 콘텐츠는 서버 렌더링이지만 동적 인터랙션(장바구니 추가, 라이브 검색, 댓글)이 필요한 곳.
htmx를 쓰면 SEO용 서버 렌더링은 유지하면서 필요한 부분에만 인터랙티비티를 붙일 수 있어요:
<!-- 페이지 리로드 없는 장바구니 추가 --> <button hx-post="/cart/add" hx-vals='{"productId": "abc123", "qty": 1}' hx-target="#cart-count" hx-swap="innerHTML"> 장바구니에 담기 </button> <!-- 라이브 댓글 섹션 --> <div hx-get="/posts/42/comments" hx-trigger="load, every 30s" hx-swap="innerHTML"> <!-- 댓글이 여기에 로드됨 --> </div>
성능 비교
서버 기반 앱에서 htmx는 중요한 성능 지표 전부에서 이깁니다:
| 지표 | htmx | React SPA |
|---|---|---|
| JavaScript 번들 | ~14 KB (htmx 자체) | 150-400 KB (프레임워크 + 앱) |
| Time to Interactive | 거의 즉시 (하이드레이션 없음) | 지연 (하이드레이션 필요) |
| INP 점수 | 우수 (최소한의 JS 실행) | 가변적 (컴포넌트 트리에 따라 다름) |
| 서버 부하 | 높음 (HTML 렌더링) | 낮음 (JSON 반환) |
| CDN 캐시 가능성 | 우수 (HTML 조각 캐시 용이) | 낮음 (동적 JSON 응답) |
React가 이기는 유일한 부분은 서버 부하. HTML을 서버에서 렌더링하면 JSON 직렬화보다 CPU를 더 잡아먹거든요. 근데 2026년 기준으로 엣지 컴퓨팅이랑 스트리밍 HTML이 보편화되면서, 이 트레이드오프는 점점 htmx 쪽에 유리해지고 있어요.
React가 여전히 대체 불가인 영역
반대편 이야기도 해야죠. htmx가 진짜로 상대가 안 되는 영역이 있는데, 이걸 안 다루면 공정하지 않으니까요.
1. 리치 텍스트 에디터와 복잡한 인터랙티브 컴포넌트
Notion, 구글 Docs, 협업 화이트보드 같은 걸 만들려면 마이크로초 단위로 복잡한 로컬 상태를 다뤄야 합니다. 키 입력 하나, 커서 위치, 셀렉션 범위, 포맷팅까지 전부 클라이언트에서 처리해야 해요. 글자 하나 칠 때마다 서버 왕복? 쓸 수 없는 수준의 레이턴시가 됩니다.
React(또는 Svelte/Vue)가 제공하는 것들:
- 효율적 배치 업데이트를 위한 Virtual DOM
- 입력 관리를 위한 Controlled Component 패턴
- 성능을 위한 세밀한 리렌더링
- 협업 프로토콜(CRDT, OT) 연동
htmx로는 이걸 못 합니다. 키스트로크마다 200ms 서버 왕복은 실용적이지 않아요. 이건 클라이언트 앱의 영역입니다.
2. 오프라인 우선 앱
인터넷 없이도 동작해야 하는 앱이라면 — 현장 서비스 앱, 모바일 노트 앱, POS 시스템 — htmx는 아키텍처적으로 호환이 안 돼요. htmx는 모든 인터랙션에 서버가 필요합니다. 서버 없으면 인터랙션 없음.
React + 서비스 워커 + IndexedDB + 동기화 로직이면 며칠간 오프라인으로 쓰다가 연결 복구 시 상태를 싱크할 수 있습니다.
3. 실시간 협업 기능
구글 Docs 스타일의 동시 편집, 멀티플레이어 게임, 협업 디자인 툴 같이 여러 유저가 높은 빈도로 공유 상태를 수정하는 경우:
- 양방향 WebSocket 연결
- 클라이언트 사이드 충돌 해결
- 옵티미스틱 로컬 상태 업데이트
- 복잡한 undo/redo 스택
htmx의 요청-응답 모델은 이 패턴에 맞지 않습니다. 로컬 상태를 유지하고 피어와 동기화하는 클라이언트 앱이 필요해요.
4. 고빈도 UI 인터랙션
드래그 앤 드롭, 인터랙티브 데이터 시각화(D3.js), 애니메이션이 많은 UI, 캔버스 기반 에디터, 게임 같은 인터페이스는 클라이언트 사이드 프레임 레이트 수준의 반응성이 필요합니다. 드래그 프레임마다 서버 왕복은 불가능해요.
의사결정 매트릭스
| 유즈 케이스 | htmx | React | 둘 다 |
|---|---|---|---|
| 어드민 대시보드 | ✅ 최적 | ⚠️ 과잉 | — |
| CRUD 앱 | ✅ 최적 | ⚠️ 과잉 | — |
| 멀티 스텝 폼 | ✅ 좋음 | ✅ 좋음 | — |
| 이커머스 | ✅ 좋음 | ✅ 좋음 | ✅ 최적 |
| 인터랙션 있는 콘텐츠 사이트 | ✅ 최적 | ⚠️ 과잉 | — |
| 리치 텍스트 에디터 | ❌ 불가 | ✅ 필수 | — |
| 오프라인 우선 앱 | ❌ 불가 | ✅ 필수 | — |
| 실시간 협업 | ❌ 부적합 | ✅ 필수 | — |
| 데이터 시각화 대시보드 | ⚠️ 제한적 | ✅ 최적 | ✅ 좋음 |
| 드래그 앤 드롭 빌더 | ❌ 불가 | ✅ 필수 | — |
"둘 다 쓰기" 패턴: 인터랙티비티 아일랜드
2026년에 가장 현실적인 답은 "둘 다 쓰기"인 경우가 많습니다. 인터랙티비티 아일랜드라는 아키텍처인데, htmx가 페이지 구조, 네비게이션, 기본 CRUD를 처리하고, 복잡한 인터랙티브 파트만 격리된 React(또는 Preact/Svelte) 컴포넌트로 처리하는 거예요.
작동 방식
<!-- htmx가 관리하는 페이지 셸 --> <main> <!-- htmx가 네비게이션 처리 --> <nav hx-boost="true"> <a href="/dashboard">대시보드</a> <a href="/analytics">분석</a> <a href="/settings">설정</a> </nav> <!-- htmx가 테이블/CRUD 처리 --> <section hx-get="/dashboard/table" hx-trigger="load" hx-target="this"> 로딩 중... </section> <!-- 인터랙티브 차트는 React 아일랜드 --> <div id="analytics-chart" data-config='{"range": "30d", "metrics": ["revenue", "users"]}'> </div> <script type="module"> import { renderChart } from './chart-island.js'; renderChart(document.getElementById('analytics-chart')); </script> </main>
이 패턴이 주는 것들:
- 빠른 페이지 로드 — 페이지 셸에 하이드레이션 없음
- SEO 친화적 — 전체 서버 렌더 HTML
- 리치 인터랙션 — 차트, 에디터 같은 위젯은 React
- 단순한 상태 — htmx가 90% 관리, React가 10% 관리
GitHub이 수년간 이 패턴을 써왔어요. GitHub 대부분은 서버 렌더 HTML이고, 코드 에디터, 파일 트리, Actions 워크플로우 빌더 같은 곳에만 JavaScript가 들어갑니다.
마이그레이션: React SPA에서 htmx 추출하기
React SPA에서 htmx로 전환을 고려 중이라면, 빅뱅 리라이트는 하지 마세요. Strangler Fig 패턴을 쓰세요. 가장 단순한 페이지부터 하나씩 교체하는 겁니다.
1단계: 인터랙션이 적은 페이지 식별
React 앱을 점검해보세요. 본질적으로 이런 페이지를 찾습니다:
- 필터가 달린 데이터 테이블
- 설정 폼
- 상세 보기
- 리스트/그리드 뷰
이 페이지들이 마이그레이션 후보예요. 전체 페이지 수의 60-80%를 차지하지만, 인터랙티브 복잡도는 20% 미만일 겁니다.
2단계: 서버 사이드 라우트
마이그레이션 후보마다 완전한 HTML을 반환하는 서버 사이드 라우트를 만듭니다:
# Django 예시 def user_list(request): users = User.objects.filter(active=True) sort = request.GET.get('sort', 'name') if request.headers.get('HX-Request'): # htmx 요청: 테이블 바디만 반환 return render(request, 'users/_table_body.html', {'users': users}) # 풀 페이지 요청: 전체 페이지 반환 return render(request, 'users/list.html', {'users': users})
HX-Request 헤더 체크가 핵심 패턴입니다. 같은 라우트, 같은 로직인데, htmx는 HTML 조각을 받고 직접 접속은 전체 페이지를 받아요.
3단계: React는 필요한 곳에 유지
인터랙티브 컴포넌트까지 옮기지 마세요. 리치 텍스트 에디터, 드래그 앤 드롭 칸반 보드, 실시간 채팅 위젯은 React 그대로 유지하고, 서버 렌더 페이지에 마운트되는 독립 아일랜드로 감싸세요.
마이그레이션 ROI
CRUD 위주 React SPA에서 htmx로 전환한 팀들이 공통적으로 보고하는 수치:
- 프론트엔드 코드 40-60% 감소 (제거된 JS/TS 라인 수 기준)
- 표준 CRUD 화면 기능 개발 속도 향상 (클라이언트 상태 관리 와이어링 불필요)
- Core Web Vitals 개선 (하이드레이션 제거로 LCP, INP 향상)
- 디버깅 간소화 (서버 로그에 전체 이야기가 있음; 클라이언트 상태 재현 불필요)
트레이드오프는 서버 렌더링 부하 증가와, 기존에 클라이언트에서 처리하던 인터랙션의 레이턴시가 살짝 높아지는 점입니다. 내부 툴이나 어드민 패널에서 이 트레이드오프는 거의 항상 가치가 있어요. 복잡한 인터랙션의 소비자 대면 SPA에서는 거의 안 맞고요.
백엔드 팩터: htmx가 빛나는 스택
htmx의 과소평가된 측면이 하나 있어요. 백엔드 팀의 잠금 해제. 팀이 주로 Go, Python(Django/Flask), Ruby(Rails), Rust, PHP(Laravel), Java(Spring) 개발자라면, htmx는 React를 배우지 않고도 인터랙티브 웹 앱을 만들 수 있게 해줍니다.
이건 "백엔드 개발자가 프론트를 못 배운다"는 얘기가 아니에요. 생산성 얘기입니다. Django 개발자가 htmx로 인터랙티브 어드민 패널을 하루 만에 출시할 수 있어요. 같은 개발자가 Next.js 세팅하고, React 패턴 배우고, TanStack Query 설정하고, API 라우트 연결하려면 일주일이 걸리겠죠.
htmx + 백엔드 언어 생태계
| 백엔드 | htmx 통합 | 템플릿 엔진 | 프로덕션 사례 |
|---|---|---|---|
| Go | 우수 (templ, 표준 라이브러리) | templ, html/template | 인프라 대시보드 |
| Python/Django | 우수 (django-htmx) | Django 템플릿 | 어드민 패널, CMS |
| Ruby/Rails | 우수 (Turbo + htmx) | ERB, Haml | SaaS 관리 인터페이스 |
| Rust/Axum | 성장 중 (askama, maud) | askama, maud | 고성능 대시보드 |
| PHP/Laravel | 우수 (네이티브 Blade) | Blade | 이커머스 관리, CRM |
| Java/Spring | 좋음 (Thymeleaf) | Thymeleaf | 엔터프라이즈 CRUD |
눈에 보이는 패턴이 있어요. htmx 채택률이 높은 곳은 원래 서버 사이드 렌더링이 기본이었고, "여기에 인터랙션만 좀 추가하고 싶은데..."가 제일 큰 고민이었던 생태계들이에요.
htmx 초보가 자주 하는 실수 (그리고 해결법)
1. htmx를 클라이언트 프레임워크처럼 쓰기
가장 흔한 실수가 클라이언트에서 상태를 관리하는 거예요. 어떤 탭이 활성인지, 필터가 뭐가 적용됐는지, 어떤 항목이 선택됐는지를 JavaScript로 추적하고 있다면, htmx의 아키텍처와 싸우고 있는 겁니다.
이러면 안 돼요:
// 이렇게 하지 마세요 let activeFilters = []; document.querySelectorAll('.filter-checkbox').forEach(cb => { cb.addEventListener('change', () => { if (cb.checked) activeFilters.push(cb.value); else activeFilters = activeFilters.filter(f => f !== cb.value); htmx.ajax('GET', `/items?filters=${activeFilters.join(',')}`, '#results'); }); });
이렇게 하세요:
<!-- 서버가 필터 상태를 관리하게 맡기기 --> <form hx-get="/items" hx-target="#results" hx-trigger="change"> <label><input type="checkbox" name="filter" value="active"> 활성</label> <label><input type="checkbox" name="filter" value="premium"> 프리미엄</label> <label><input type="checkbox" name="filter" value="new"> 신규</label> </form>
폼이 자동으로 직렬화돼요. 서버가 필터 파라미터를 읽고, DB를 조회하고, HTML을 반환하면 끝. 클라이언트 상태 관리가 필요 없습니다.
2. 요청을 너무 많이 보내기
htmx가 서버 요청을 너무 쉽게 만들어주니까, 자칫하면 요청 폭탄 앱이 돼버려요. hx-trigger 수정자로 디바운스와 스로틀을 걸어두세요:
<!-- 검색 인풋 디바운스 --> <input hx-get="/search" hx-trigger="input changed delay:300ms" hx-target="#results" name="q"> <!-- 스크롤 기반 로딩 스로틀 --> <div hx-get="/feed/next" hx-trigger="revealed throttle:500ms" hx-swap="afterend"> </div>
3. 로딩 상태를 잊어버리기
htmx는 로딩 상태를 위한 hx-indicator 패턴을 제공합니다. 빠짐없이 쓰세요:
<button hx-post="/process" hx-indicator="#spinner"> 제출 <img id="spinner" class="htmx-indicator" src="/spinner.svg"> </button>
.htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: inline; } .htmx-request.htmx-indicator { display: inline; }
2026년 현실 점검
업계가 수렴하고 있는 현실적인 중간 지점:
-
SPA는 죽지 않았습니다. 복잡하고 인터랙티브한 앱에는 여전히 클라이언트 프레임워크가 필요해요. React, Vue, Svelte 안 사라집니다.
-
SPA가 남용되고 있습니다. 본질적으로 서버 기반 CRUD인 앱이 너무 많이 클라이언트 SPA로 만들어졌어요. "요새 다 이렇게 하니까"라는 이유로. htmx가 이 과잉 엔지니어링을 드러내는 거예요.
-
최고의 아키텍처는 문제에 맞는 아키텍처입니다. Notion 클론에는 React가 필요하고, 어드민 대시보드에는 htmx가 필요하고, SaaS 제품에는 둘 다 필요할 수 있어요.
-
htmx는 "과거로 돌아가기"가 아닙니다. PHP 시절 개발이 아니에요. HTTP 시맨틱스, 모던 브라우저, 대부분의 인터랙션이 요청-응답 패턴이라는 사실을 활용하는 현대적 접근입니다. 2026년의 서버 렌더링 인프라(엣지 함수, 스트리밍 HTML, CDN 캐시 프래그먼트)가 이 방식을 그 어느 때보다 빠르고 확장 가능하게 해줘요.
-
JavaScript 생태계 피로감은 현실입니다. htmx의 성장이 순수하게 기술적 장점 때문만은 아니에요. 200MB
node_modules유지보수하고, webpack 설정 디버깅하고, 6개월마다 새 상태 관리 라이브러리를 배우는 데 지친 개발자들도 한몫하고 있습니다.
프로젝트가 실제로 뭘 필요로 하는지에 기반해서 아키텍처를 고르세요. 트위터에서 뭐가 힙하다는지 말고요. 대부분의 웹 앱에서 정답은 JavaScript를 더 쓰는 게 아니라, 덜 쓰는 겁니다.
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요