생산성

HTML-first 웹 개발 생산성 가이드: JS 의존 줄이는 전환 체크리스트

React 번들 비대화와 빌드 지연을 해결하는 HTML-first 전환 전략. HTMX 2.0, Alpine.js, Web Components 선택 기준과 실전 체크리스트로 개발 속도를 되찾는 방법을 정리했다.

HTML-first 웹 개발 생산성 가이드: JS 의존 줄이는 전환 체크리스트

React 앱을 만들다 보면 어느 순간 npm run build가 30초를 넘고, 번들 크기는 1MB를 뚫고, 신규 팀원이 컴포넌트 구조를 파악하는 데 이틀이 걸린다. 문제는 대부분 “JavaScript가 필요 없는 곳에 JavaScript를 쌓아왔다”는 데 있다. HTML-first는 이 방향을 뒤집는다 — 브라우저가 이미 할 수 있는 일은 브라우저에 맡기고, JS는 진짜 필요한 곳에만 쓴다.


HTML-first가 생산성을 높이는 이유

JavaScript 번들이 비대해지면 빌드 시간만 늘어나는 게 아니다. 상태 관리 라이브러리 하나가 들어오면 그걸 래핑하는 훅, 훅을 쓰는 컴포넌트, 컴포넌트를 조합하는 페이지 레이어가 함께 따라온다. 버그 하나를 고치려면 세 파일을 열어야 한다. 코드 리뷰는 JSX 트리를 해독하는 시간이 되고, 디자이너나 콘텐츠 담당자는 아예 손을 못 댄다.

브라우저 표준은 최근 몇 년 사이 이 격차를 빠르게 좁혔다. Declarative Shadow DOM2024년 8월 5일 Chrome·Firefox·Safari 모두에서 지원되어 Baseline Newly Available이 됐고, 2026년 8월이면 Baseline Widely Available에 도달한다. 커스텀 엘리먼트를 서버에서 HTML 문자열로 렌더링할 수 있게 된 것이다. <template shadowrootmode="open"> 한 줄이 hydration 전략을 통째로 단순하게 만든다.

State of HTML 2025 설문(응답자 6,223명)에서 75%가 “브라우저 상호운용성이 전보다 빠르게 개선되고 있다”고 답했다. 개발자들이 가장 원하는 기능은 커스터마이저블 셀렉트와 HTML 재사용 — 이 두 가지는 JS 라이브러리 없이 달성하고 싶다는 방향성을 보여준다.

선언적 마크업은 협업 구조도 바꾼다. hx-get="/api/items" 같은 속성은 코드 리뷰에서 의도를 즉시 드러낸다. 팀에 백엔드 개발자나 비개발자가 섞여 있을 때 이 차이는 크다.


전환 전 필수 진단: JS 과의존 구간 찾기

전환 전에 어디가 문제인지 먼저 눈으로 봐야 한다. Webpack 프로젝트라면 webpack-bundle-analyzer를, Vite라면 rollup-plugin-visualizer를 devDependencies에 추가하고 빌드해보자.

# Vite
npm i -D rollup-plugin-visualizer
# vite.config.ts에 plugins: [visualizer({ open: true })] 추가 후
npm run build

트리맵에서 실제 쓰이지 않는 라이브러리가 차지하는 면적을 보면 방향이 잡힌다. 다음 4분류로 프로젝트의 상호작용을 나눠보자.

  1. 서버 데이터 fetch — axios, SWR, React Query로 하고 있는 것
  2. 폼 제출 — 제출·유효성 검증·에러 표시
  3. UI 상태 — 모달 열고 닫기, 탭 전환, 드롭다운 토글
  4. 복잡 클라이언트 로직 — 실시간 협업, 캔버스, 복잡 애니메이션

13은 HTML-first 도구로 대체 가능하다. 4만 JS가 진짜 필요한 영역이다. 프로젝트 전체를 훑어 **jQuery, axios, 소형 UI 컴포넌트 라이브러리(Headless UI, Radix의 일부)**가 13번 용도로만 쓰이고 있다면 제거 후보다.


상황별 도구 선택: HTMX·Alpine.js·Lit

세 도구는 용도가 겹치지 않는다. 상황에 따라 하나만 쓸 수도, 셋을 함께 쓸 수도 있다.

HTMX는 서버가 HTML 파셜을 내려줄 수 있을 때 빛난다. 현재 안정 버전은 2.0.10이다. hx-get, hx-post, hx-swap, hx-target — 이 네 속성이 전체 fetch 흐름을 커버한다. 2026년 5월 22일 공개된 4.0.0-beta4는 내부적으로 XMLHttpRequest를 Fetch API로 교체했다. hx-push-url, hx-history 동작에 일부 변화가 있으므로 프로덕션 전환은 정식 릴리스 이후가 안전하다.

Alpine.js는 빌드 도구 없이 CDN 한 줄로 시작한다. x-data, x-show, x-model, x-on — 네 디렉티브로 탭·토글·폼 상태 대부분을 처리한다. 백엔드 팀이 Blade나 Jinja 템플릿을 쓰는 환경에서 특히 잘 맞는다.

Lit 3.3.3은 격리된 재사용 위젯이 필요할 때 쓴다. Shadow DOM 캡슐화로 스타일 충돌이 없고, Declarative Shadow DOM 덕분에 SSR도 지원된다. 디자인 시스템 컴포넌트를 프레임워크 독립적으로 배포해야 한다면 Lit이 정답이다.

상황선택
서버에서 HTML 파셜 내려줌HTMX
인라인 토글·탭·간단한 폼 유효성Alpine.js
재사용 위젯, 디자인 시스템Lit + Web Components
복잡 대시보드·실시간 협업React/Vue 유지

단계별 전환 실행 체크리스트

전체를 한 번에 바꾸려 하면 실패한다. 작은 구간부터 교체하고, 각 단계에서 실제로 동작하는지 확인한 뒤 다음으로 넘어간다.

0단계: 정적 콘텐츠 렌더링에서 JS 제거 클라이언트에서 fetch해서 렌더링하는 목록이 있는데 데이터가 자주 안 바뀐다면, SSR/SSG로 이동하거나 <template> 태그를 활용한다. document.getElementById('tmpl').content.cloneNode(true) 수준이면 fetch 자체가 필요 없는 경우도 많다.

1단계: 데이터 fetch를 HTMX로 대체 axios.get('/api/items').then(...)hx-get="/api/items" hx-target="#list" hx-swap="innerHTML". 서버는 JSON 대신 HTML 파셜을 응답하도록 엔드포인트를 추가한다. 기존 JSON 엔드포인트는 그대로 두고 파셜 전용 경로(/api/items/partial)를 병행 운영하면 마이그레이션 리스크가 줄어든다.

2단계: 폼 제출·실시간 검증 처리 hx-post="/form" hx-target="#result"로 제출하고, 서버는 성공/에러 HTML을 내려준다. 필드 수준 실시간 검증은 Alpine.js의 x-modelx-show로 조합한다.

<form x-data="{ email: '', valid: false }"
      hx-post="/subscribe" hx-target="#msg">
  <input x-model="email"
         x-on:input="valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)"
         type="email" name="email">
  <span x-show="!valid && email.length > 0" class="error">이메일 형식 확인</span>
  <button :disabled="!valid">구독</button>
</form>

3단계: 모달·탭·드롭다운을 네이티브 API로 <dialog> 엘리먼트와 CSS :open 의사 클래스를 쓰면 JS 없이 모달 스타일링이 된다. 열기는 dialog.showModal(), 닫기는 method="dialog" 폼 버튼으로 처리. HTMX와 조합하면 hx-on::after-request="document.getElementById('modal').showModal()" 한 줄로 서버 응답 후 모달을 열 수 있다.

4단계: 반복 재사용 UI를 Lit 커스텀 엘리먼트로 분리 사이트 전체에서 쓰이는 버튼, 뱃지, 카드 같은 요소를 Lit으로 정의한다. Declarative Shadow DOM 지원 덕분에 서버 템플릿에서 <template shadowrootmode="open"> 을 직접 렌더링해 hydration 없이 초기 페인트가 가능하다.


실전 패턴: 핵심 시나리오 코드로 보기

무한 스크롤hx-trigger="revealed" 한 속성이면 충분하다. 뷰포트에 엘리먼트가 진입하면 자동으로 요청한다.

<div hx-get="/items?page=2"
     hx-trigger="revealed"
     hx-swap="afterend">
  <p>로딩 중...</p>
</div>

폼 피드백 + 로딩 인디케이터hx-indicator에 CSS 클래스를 연결하면 요청 중 스피너가 자동으로 표시된다.

<button hx-post="/save"
        hx-target="#status"
        hx-indicator="#spinner">저장</button>
<span id="spinner" class="htmx-indicator">저장 중...</span>
<div id="status"></div>

Alpine.js 다중 탭

<div x-data="{ tab: 'overview' }">
  <button x-on:click="tab = 'overview'" :class="{ active: tab === 'overview' }">개요</button>
  <button x-on:click="tab = 'detail'" :class="{ active: tab === 'detail' }">상세</button>
  <div x-show="tab === 'overview'">개요 내용</div>
  <div x-show="tab === 'detail'">상세 내용</div>
</div>

이 패턴들의 공통점은 상태가 HTML 속성이나 서버에 있다는 것이다. 디버깅할 때 DevTools Elements 탭만 봐도 현재 상태를 알 수 있다.


함정과 주의사항: JS를 유지해야 할 경우

SEO와 크롤링 — HTMX로 교체되는 콘텐츠가 Google 색인에 들어가야 한다면 초기 HTML에 포함되어 있어야 한다. hx-swap으로 동적 교체되는 영역은 Googlebot이 JavaScript를 실행하지 않아 놓칠 수 있다. 핵심 콘텐츠는 SSR로 초기 마크업에 포함시키고, HTMX는 페이지네이션·필터 같은 탐색 보조에만 쓰는 게 안전하다.

HTMX 4.0-beta는 아직 프로덕션 금지 — 2026년 5월 22일 공개된 4.0.0-beta4는 Fetch API 전환 외에도 이벤트 네이밍과 히스토리 동작이 일부 바뀌었다. 안정 버전인 2.0.10을 쓰고, 4.0 정식 릴리스 전에는 스테이징에서만 테스트하자.

HTML-first가 통하지 않는 영역이 있다 — Google Docs 수준의 실시간 협업, WebSocket 양방향 동기화가 핵심인 앱, 60fps 애니메이션이 필요한 인터랙티브 데이터 시각화는 React나 Svelte가 맞다. HTML-first를 선택해도 이 영역만 Vue/React로 남기는 하이브리드가 현실적이다.

hx- 남용 방지* — HTMX 속성이 늘어나면 HTML 파일이 오히려 읽기 어려워진다. 팀 컨벤션으로 hx- 속성은 data-hx- 네임스페이스로 묶거나, 반복되는 패턴은 <template>으로 분리하는 규칙을 세우는 것이 좋다.


자주 묻는 질문

Q. HTMX를 기존 React 앱과 같이 쓸 수 있나요?

같이 쓸 수 있지만 역할 분리가 중요하다. React가 관리하는 DOM 영역에 HTMX 속성을 붙이면 두 라이브러리가 DOM을 동시에 조작해 충돌이 발생한다. 실용적인 전략은 React 컴포넌트 트리 바깥의 정적 페이지 영역(헤더, 사이드바, 랜딩 섹션)에 HTMX를 적용하고, React는 복잡한 인터랙티브 구간에만 유지하는 것이다.

Q. Alpine.js와 HTMX는 어떻게 역할을 나누나요?

HTMX는 네트워크 계층 — 서버에 요청하고 HTML 응답을 DOM에 끼워 넣는 일을 한다. Alpine.js는 클라이언트 상태 계층 — 탭 활성화, 폼 유효성, 드롭다운 열림 여부 같이 서버 왕복 없이 처리되는 로컬 상태를 다룬다. 둘은 겹치지 않으므로 같은 엘리먼트에 함께 써도 무방하다. <div hx-get="/data" x-data="{ open: false }"> 형태가 자연스럽다.

Q. Web Components는 IE나 구형 Android에서 동작하나요?

커스텀 엘리먼트와 Shadow DOM v1은 Chrome 67+, Firefox 63+, Safari 10.1+에서 지원된다. 2024년 기준 IE는 공식 지원 종료 상태이고, Android는 Chrome 기반이므로 실질적으로 지원 범위에 들어온다. Lit 자체도 ES2021을 타깃으로 하며, 구형 환경이 필요하면 Lit의 @lit/reactive-element 레거시 빌드 타깃을 쓸 수 있다.

Q. 서버 파셜 응답을 강제하면 API 재사용성이 떨어지지 않나요?

JSON API는 그대로 두고, 별도 /partial 경로에서 HTML을 응답하는 방식으로 양쪽을 유지하는 게 일반적이다. Django의 경우 request.headers.get('HX-Request') 헤더로 HTMX 요청을 구분해 같은 뷰에서 JSON/HTML 두 형식을 분기할 수 있다. Rails는 respond_to 블록으로 동일하게 처리한다.

RELATED · 관련 글

이어 읽기 좋은 글

생산성

Claude로 UI 디자인하기: Figma 없이 프로토타입부터 컴포넌트까지

2026.06.08 · 13분
생산성

마우스리스 키보드 생산성 완전 가이드: 단축키로 작업 속도 2배 높이기

2026.06.06 · 16분
생산성

크리에이틴 뇌 기능 향상 완전 가이드: 지식 근로자 복용법과 인지 저하 예방

2026.06.01 · 9분