- 사람들은 htmx가 SPA에서 웹을 구원하는 것처럼 말함
- htmx 제작자 Carson Gross는 이러한 역학을 "헤겔의 변증법"으로 위트 있게 설명함:
- 정립(thesis): 전통적인 MPA
- 반정립(antithesis): SPA
- 종합(synthesis): 하이퍼미디어 기반의 인터랙티브한 섬들로 구성된 애플리케이션
- 하지만 나는 이 걸 못보고, 예전에 "htmx로 SPA를 만들었음"
- 단순한 ToDo 리스트앱의 PoC임
- 페이지가 로드되면 서버와 더 이상 통신하지 않음
- 모든 것이 클라이언트에서 로컬로 처리됨
- htmx가 네트워크를 통한 하이퍼미디어 교환 관리에 초점을 맞추고 있다면 이건 어떻게 작동하는 걸까?
- 한 가지 단순한 트릭: "Server-Side" 코드가 Service Worker에서 실행됨
서비스 워커
- 웹페이지와 인터넷 사이에서 프록시로 동작함
- 네트워크 요청을 가로채고 조작할 수 있음
- 요청을 변경, 오프라인용 응답 캐싱, 브라우저 밖으로 요청을 보내지 않고 새 응답 생성 가능
- 마지막 기능이 이 단일 페이지 앱의 핵심
- htmx로 네트워크 요청하면 서비스 워커가 가로챔
- 그 다음 서비스 워커는 비즈니스 로직을 실행하고 새 HTML을 생성
- htmx가 새 HTML을 DOM에 교체함
기존 SPA 대비 장점
- 서비스 워커는 IndexedDB를 스토리지로 사용해야 함
- 이는 페이지 로드 간에 상태를 유지함
- 페이지를 닫았다가 다시 와도 앱이 데이터를 기억함
- 이는 이 아키텍처를 선택하면 "무료"로 제공되는 부수적인 이점
- 오프라인에서도 동작하도록 만들기 쉬움
단점
- 개발자 도구 지원이 열악함
-
console.log
를 간헐적으로 누락
- 서비스 워커 설치 여부 보고가 신뢰할 수 없음
- Firefox에서 ES 모듈 지원 부재
- 일반적인 개발 경험이 "재미없음"
그럼에도 불구하고 htmx SPA는 잘 동작함
구현 세부 사항
- 기본 HTML은 단일 페이지 앱의 빈 틀
-
<body>
태그가 htmx를 이용해 앱의 본체를 설정함
-
hx-boost="true"
: htmx가 전체 페이지 네비게이션 없이 링크 클릭과 폼 제출 응답을 Ajax로 교체하도록 지시
-
hx-push-url="false"
: htmx가 링크 클릭과 폼 제출에 따라 URL을 변경하지 않도록 함
-
hx-get="./ui"
: 페이지 로드 시 /ui
에서 페이지를 가져와 교체하도록 지시
-
hx-target="body"
: 결과를 <body>
요소에 교체하도록 지시
-
hx-trigger="load"
: 페이지 로드 시 이 모든 작업을 수행하도록 지시
/ui
엔드포인트
- 앱의 실제 마크업을 반환함
- 이후 htmx가 링크와 폼을 제어하여 인터랙티브하게 만듦
- 서비스 워커가 Express 스타일 라이브러리로 요청 라우팅 처리
-
setFilter
와 listTodos
는 IDB Keyval을 래핑한 간단한 함수
-
App
컴포넌트는 필터 폼, 할 일 목록, 추가 폼으로 구성됨
/todos/add
엔드포인트
- 할 일 저장 후 UI를 다시 렌더링한 응답을 반환
- htmx가 응답을 DOM에 교체함
Todo
컴포넌트
- 체크박스, 삭제 버튼, 할 일 텍스트로 구성
- 체크박스는
/todos/${id}/update
요청 트리거
- 삭제 버튼은
/todos/${id}
삭제 요청 트리거
- 할 일 텍스트는 "normal"과 "editing" 두 가지 상태
- htmx가 더블 클릭 이벤트 수신
-
/ui/todos/${id}?editable=true
요청
- 서비스 워커가
<input>
이 포함된 Todo
HTML 반환
- htmx가 응답의 HTML로 현재 할 일 항목 교체
요약
- 기술적으로는 동작함
- 좋은 아이디어일까? 과연 하이퍼미디어 기반 앱의 절정일까? React를 버리고 이렇게 만들어야 할까?
- 완전히 로컬인 앱에서는 htmx의 간접성이 해방감보다는 부담으로 느껴짐
- 대부분의 앱은 완전히 로컬이 아님
- "인터랙티브한 섬들(islands of interactivity)" 패턴이 "서버 측" 코드를 서비스 워커와 실제 서버로 나누는 것보다 낫다고 봄
- 하이퍼미디어로 완전히 로컬인 단일 페이지 앱을 구축하는 모습을 보여주려는 실험적 시도였음