Go에서 HTMX를 활용하는 방법
(alexedwards.net)- Go의
html/template기반 서버 측 렌더링을 유지하면서 HTMX로 동적 상호작용을 더하고, 하나의 렌더러에서 전체 페이지와 HTML 조각을 선택적으로 반환함 base.tmpl, 페이지 템플릿, 재사용 조각으로 마크업을 나누고 Go 1.16의embed.FS와 공유 템플릿 집합을 활용해 배포와 렌더링 구조를 단순화함HX-Request에 따라 검색 결과를 전체 페이지 또는 테이블 행으로 반환하며,Vary: HX-Request와historyRestoreAsHxRequest: false로 캐시 및 뒤로 가기 복원 오류를 방지함- HTMX 요청을 다른 페이지로 이동시킬 때는 일반 3xx 대신
HX-Redirect와 2xx 응답을 사용하고, 204·422·4xx·5xx별responseHandling으로 교체 범위와 오류 표시를 제어함 - 로컬 저장소 캐시와 속성 상속을 끄고 요청 제한 시간을 설정하는 구성을 출발점으로 삼으며, 규모가 커지면 중간 레이아웃 템플릿을 추가해 관리자 영역 같은 별도 화면 구조까지 지원할 수 있음
Go의 서버 렌더링과 HTMX를 결합하는 이유
- HTMX는 적은 JavaScript만으로 동적 상호작용을 추가하면서 Go
html/template의 서버 측 HTML 렌더링이 주는 일관성과 안전성을 유지하게 해줌 - 구현에서 다루는 축은 세 가지임
- 전체 페이지와 부분 HTML 응답을 함께 지원하는 템플릿 구조
- HTMX 환경의 리다이렉트와 오류 처리
- HTMX 기본값을 변경하는 구성과 그 이유
- 버튼을 이미지로 교체하는 기본 기능에서 시작해, 사용자 이름이나 이메일을 입력하는 동안 목록을 필터링하는 검색 화면으로 확장함
- 같은 템플릿 패턴은 HTMX뿐 아니라 Unpoly와 Hotwire 같은 HTML-over-the-wire 도구에도 적용 가능함
- HTMX의 기본 사용법을 모른다면 먼저 공식 문서를 살펴보는 편이 적합함
프로젝트 디렉터리와 정적 파일 구성
- 프로젝트는 역할에 따라 다음 영역으로 나뉨
assets/html/base.tmpl: 모든 페이지가 공유하는 HTML 골격assets/html/pages/: 개별 페이지 콘텐츠assets/html/partials/: 여러 위치에서 재사용하는 HTML 조각assets/static/css,assets/static/img,assets/static/js: 정적 자산cmd/web/: 서버 시작, 핸들러, HTML 렌더러 코드
- 기본 골격은 다음 명령으로 생성함
go mod init example.com/htmx
mkdir -p assets/static/css assets/static/img assets/static/js assets/html/partials assets/html/pages cmd/web
touch assets/efs.go assets/html/base.tmpl assets/html/partials/images.tmpl assets/html/pages/home.tmpl cmd/web/main.go cmd/web/handlers.go cmd/web/html.go
- HTMX는 CDN이나 NPM으로 설치할 수도 있지만, 복사본을 내려받아 애플리케이션의 정적 파일로 직접 제공하는 방식을 주로 사용함
- 구성이 단순하며 CDN 사용에 따른 단점을 피할 수 있음
htmx.org@2.0.10과 클래스 없는 CSS 프레임워크bamboo.css@1.4.0을 사용함
wget -P assets/static/js https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js
wget -P assets/static/css https://cdn.jsdelivr.net/npm/bamboo.css@1.4.0/dist/bamboo.min.css
기본·페이지·부분 템플릿 분리
base.tmpl은<head>, 공통 제목,<main>등 전체 문서 골격을 정의하고page:title과page:content를 삽입함- Bamboo CSS와 HTMX 스크립트는 기본 템플릿에서 불러오며, HTMX 스크립트에는
defer속성을 지정함- 브라우저가 HTML을 파싱하는 동안 스크립트를 병렬로 가져옴
- HTML 파싱과 DOM 구성이 끝난 뒤 스크립트를 실행함
- 모든 템플릿은 파일명에 의존하지 않고
{{define}}...{{end}}로 명시적인 이름을 부여함- Go 코드는 정의된 이름만으로 템플릿을 일관되게 참조할 수 있음
page:title의:는 임의의 구분자이며page_title,page-title,pageTitle,title같은 이름도 가능함
- 홈페이지 버튼에는 두 가지 동작을 지정함
hx-get="/gopher": 클릭할 때GET /gopher요청을 보냄hx-swap="outerHTML": 응답 HTML로 버튼 자체를 교체함
partial:image:gopher는 재사용 가능한 이미지 조각이며,width="{{.}}"를 통해 호출 시점에 이미지 너비를 전달받음- 특정 페이지에서만 사용하는 HTML 조각은 공용
partials디렉터리가 아니라 해당 페이지 파일에 둘 수 있음- 여러 페이지에서 쓰는 조각은 공용 디렉터리에 두고, 페이지 전용 조각은 관련 콘텐츠 옆에 배치하는 구분임
Go 바이너리에 HTML과 정적 자산 포함
- Go 1.16에 파일 임베딩이 추가된 뒤에는 런타임에 디스크에서 읽는 대신 HTML과 정적 자산을 Go 바이너리에 포함할 수 있음
//go:embed "html" "static"지시어로 두 디렉터리를embed.FS에 넣고,fs.Sub()를 사용해 두 개의 하위 파일 시스템으로 분리함HTMLFiles:html을 루트로 사용하는 템플릿 파일 시스템StaticFiles:static을 루트로 사용하는 정적 파일 시스템
- 분리해서 얻는 이점은 두 가지임
- HTML 코드와 정적 파일 코드가 서로의 파일에 불필요하게 접근하지 않음
- 파일을 열 때
html/또는static/접두사를 붙일 필요가 없음
sub()에서panic()을 사용하지 않으려면 오류를 반환하고main()에서 두 변수를 초기화할 수도 있음- 현재 구현에서
fs.Sub()가 실패하려면 디렉터리 값이 유효한 경로가 아니어야 하지만, 고정 문자열"html"과"static"은 이 검사를 통과하므로 런타임 패닉 위험이 매우 낮음
공유 템플릿 집합과 htmlRenderer
htmlRenderer는 렌더링 흐름을 다음처럼 관리함- 시작할 때 공유 템플릿 집합을 한 번 파싱함
- 렌더링할 때마다 공유 집합을 복제함
- 요청에 필요한 페이지 템플릿을 추가로 파싱함
- 지정한 이름의 템플릿을 실행해 HTTP 응답으로 전송함
newHTMLRenderer()는template.FuncMap을 등록한 뒤 공통 템플릿을 파싱함now를time.Now에 연결함- 다른 사용자 정의 템플릿 함수도 같은 위치에 추가할 수 있음
render()는sharedTemplates.Clone()으로 요청별 템플릿 집합을 만들고,additionalTemplateFiles가 있으면ParseFS()로 확장함- 템플릿은 먼저
bytes.Buffer에 실행한 뒤 상태 코드를 기록하고 응답 본문으로 전송함 - 서버 초기화 시
"base.tmpl"과"partials/*.tmpl"을 공유 집합으로 파싱함- 기본 템플릿과 모든 공용 조각을 렌더러에서 항상 사용할 수 있음
- 페이지별 템플릿만 핸들러 호출 시 추가함
- 정적 파일은
http.FileServerFS(assets.StaticFiles)와/static/경로로 제공하며, 서버는:5051에서 실행됨
전체 페이지와 HTML 조각 렌더링
- 홈페이지 핸들러는 다음 호출로 전체 문서를 반환함
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
- 공유 집합에
pages/home.tmpl을 추가하고base템플릿을200 OK로 실행하는 호출임 /gopher핸들러는 추가 파일을 파싱하지 않고, 공유 집합에 이미 포함된partial:image:gopher만 실행함
width := 100
app.html.render(w, http.StatusOK, width, "partial:image:gopher")
- HTMX는
/gopher에서 받은 이미지를hx-swap="outerHTML"설정에 따라 버튼과 교체함 - 이 구조는 다음 이점을 제공함
- 템플릿과 정적 자산이 바이너리에 포함돼 배포가 쉬워짐
- 동일한
render()로 전체 HTML 페이지와 특정 HTML 조각을 모두 반환할 수 있음 - 기본 템플릿과 부분 템플릿으로 마크업 중복을 줄임
- 부분 템플릿을 기본 템플릿, 페이지 콘텐츠, 다른 부분 템플릿에서도 재사용 가능함
입력과 동시에 갱신되는 사용자 검색
- 검색 기능은 두 경로로 나뉨
GET /users: 모든 사용자 정보를 담은 전체 HTML 페이지GET /users/search: 이름이나 이메일이 검색어와 일치하는 사용자의 테이블 행만 반환하는 HTML 조각
- 검색 입력창은 다음 HTMX 속성을 조합함
hx-get="/users/search"로 검색 요청을 보냄hx-trigger="input changed delay:500ms, keyup[key=='Enter']"로 입력 변경 후 500ms가 지나거나 Enter를 누르면 요청함- 검색어는
GET /users/search?query=foo형태의 쿼리 문자열로 전달됨 hx-target="#search-results"로 응답을<tbody id="search-results">내부에 넣음hx-push-url="true"로 요청마다 주소 표시줄을 갱신하고 브라우저 기록에 항목을 추가함
users:rows는 사용자 행만 렌더링하는 별도 템플릿임- 각 행에는 이름과 이메일이 표시됨
IsGopher가 참이면partial:image:gopher를 너비 24로 렌더링함
listUsers와searchUsers는 모두pages/users.tmpl을 공유 집합에 추가하되 실행하는 템플릿은 다름listUsers:base를 실행해 전체 페이지를 반환함searchUsers:users:rows를 실행해 일치하는 행만 돌려줌
- 검색어가 비어 있으면 전체 사용자 목록을 사용하고, 값이 있으면
strings.Contains()로 이름과 이메일을 검사함 - 실제 애플리케이션에서는 다음 보완을 고려할 수 있음
- 목록과 검색 핸들러를 하나로 합침
- 오류 처리와 로깅 도우미를 중앙화함
- 미들웨어로 Content Security Policy 헤더와 패닉 복구를 추가함
- 서버 제한 시간을 적절히 설정함
HX-Request로 전체 페이지와 부분 응답 구분
- 사용자가
/users/search?query=leo를 브라우저에서 직접 열거나 공유받은 링크로 방문하면, 테이블 행에 해당하는 부분 HTML만 보일 수 있음 - HTMX 요청에는 항상
HX-Request: true헤더가 포함되므로 이를 기준으로 응답 형태를 결정함
func isHTMXRequest(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}
- 검색 핸들러의 기본 실행 템플릿을
base로 두고, HTMX 요청일 때만users:rows로 변경함- 직접 방문하면 검색 결과가 포함된 전체 페이지를 받음
- HTMX 요청에는
<tbody>에 넣을 행 조각만 반환함
Vary 헤더와 뒤로 가기 복원
- 같은 URL이
HX-Request값에 따라 서로 다른 응답을 반환하므로 캐시에도 이 차이를 알려야 함 - 모든 렌더링 응답에
Vary: HX-Request를 추가함- 개별 핸들러에서 필요한 경우에만 설정하는 편이 엄밀히는 낭비가 적음
- 렌더러에서 일괄 설정하면 누락으로 발생할 수 있는 버그를 피할 수 있음
hx-push-url이나hx-boost가 브라우저 기록을 추가하면 HTMX는 완성된 페이지 HTML을 브라우저 로컬 저장소에 캐시함- 기본 캐시는 최대 10페이지임
- 캐시에 없는 지점까지 뒤로 이동하면 HTMX가 서버에 해당 URL을 다시 요청함
- 기본 복원 요청에는
HX-Request: true가 포함되므로 서버가 전체 페이지 대신 부분 HTML을 반환할 수 있음 HX-Request로 응답 종류를 선택한다면historyRestoreAsHxRequest: false를 설정함- 캐시 미스 복원 요청에서
HX-Request: true를 제외함 - 서버가 해당 요청에 전체 HTML 페이지를 반환하게 됨
- 캐시 미스 복원 요청에서
- HTMX 구성은
base.tmpl의htmx-config메타 태그에 넣음
HTMX 요청을 다른 페이지로 리다이렉트
- HTMX는 성공 메시지 같은 HTML 조각을 즉시 교체할 수 있어 일반적인 Post/Redirect/Get 흐름이 필요한 경우를 줄여줌
- 로그인 성공 후 프로필로 이동하는 것처럼 완전히 다른 페이지로 보내야 할 때는 일반 3xx 응답만으로 처리할 수 없음
- 브라우저가 HTMX보다 먼저 3xx 응답을 따라감
- HTMX는 리다이렉트 뒤의 최종 응답만 확인하고 그 콘텐츠를 기존 대상에 교체함
- HTMX 요청에는 2xx 응답과
HX-Redirect헤더를 함께 사용함HX-Redirect: /foo/bar는 브라우저를 해당 URL로 이동시켜 전체 페이지를 다시 불러옴- 이동 대상 요청에는
HX-Request: true가 포함되지 않음
- JavaScript가 꺼졌거나 HTMX가 로드되지 않은 환경도 지원하려면, HTMX가 아닌 요청에는 일반 3xx 응답을 유지해야 함
func redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
if isHTMXRequest(r) {
w.Header().Set("HX-Redirect", url)
w.WriteHeader(http.StatusNoContent)
return
}
http.Redirect(w, r, url, code)
}
- 로그인 뒤
/profile로 이동할 때는 다음처럼 호출할 수 있음
redirect(w, r, "/profile", http.StatusSeeOther)
HX-Redirect와 HX-Location의 차이
HX-Location은 전체 페이지를 다시 불러오지 않고 리다이렉트와 유사한 동작을 수행함- 대상 URL의 HTML을 가져옴
- 응답을
<body>에 교체함 - 브라우저 기록에 새 항목을 추가함
- 대상 URL을 요청할 때는
HX-Request: true가 포함됨 - 대상 경로가
HX-Request에 따라 부분 HTML을 반환한다면 문제가 생길 수 있음- 핸들러는
HX-Location을 따라온 요청과 일반 HTMX 요청을 구분할 수 없음 - 전자에는 전체 페이지가 필요하지만 후자에는 부분 응답이 필요함
- 핸들러는
- 히스토리 복원과 달리
HX-Location이동에서HX-Request: true를 끄는 설정은 없음 - 대부분은 전체 페이지 재로딩을 감수하고
HX-Redirect를 사용하는 편이 더 쉽고 안전함 - 이동 대상 경로가 항상 전체 HTML만 반환하도록 구성됐다면
HX-Location도 사용할 수 있음
204·422·4xx·5xx 응답 처리
- HTMX는 기본적으로 4xx 또는 5xx 응답을 DOM에 교체하지 않음
- 현재 화면을 그대로 유지함
- 오류는 개발자 도구 콘솔에 기록됨
- 존재하지 않는 템플릿 이름 때문에 서버가
500 Internal Server Error를 반환하더라도, 기본 설정에서는 사용자가 화면에서 오류를 확인하지 못함 - 오류 메시지를 사용자에게 보여주기 위해 4xx와 5xx 응답은
<body>전체에 교체하도록 설정함 - 예외는
422 Unprocessable Content임- 폼 검증 오류가 포함된 폼을 반환할 때 사용함
- 기존 HTMX 대상에 응답이 정상적으로 교체돼야 함
responseHandling은 상태 코드에 따라 다음처럼 구성함204 No Content: DOM을 변경하지 않음422 Unprocessable Content: 기존 대상에 정상 교체함- 나머지 4xx와 5xx: 응답을
<body>에 교체함 - 그 밖의 응답: 기존 대상에 정상 교체함
"responseHandling":[
{"code":"204", "swap": false},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": true, "target": "body"},
{"code":"...", "swap": true}
]
- 특정 상호작용에서 오류를
<body>가 아닌 다른 대상에 넣어야 한다면 response target 확장 기능으로 기본 설정을 덮어쓸 수 있음
브라우저 주소 표시줄의 현재 URL 확인
- HTMX는
hx-boost,hx-push-url,hx-replace-url을 사용하지 않으면 AJAX 요청과 응답 교체 과정에서 브라우저 URL을 변경하지 않음 - 이 때문에 Go 핸들러의
r.URL과 사용자가 주소 표시줄에서 보는 URL이 다를 수 있음 - HTMX는 각 요청의
HX-Current-URL헤더에 브라우저가 현재 표시하는 URL을 전달함 - 도우미 함수는 이 헤더를
url.URL로 파싱하고, 헤더가 없으면r.URL을 반환함
func browserURL(r *http.Request) (*url.URL, error) {
cu := r.Header.Get("HX-Current-URL")
if cu != "" {
return url.Parse(cu)
}
return r.URL, nil
}
HTMX 기본값을 변경하는 구성
historyCacheSize: 0으로 HTMX의 로컬 저장소 페이지 캐시를 완전히 끔- 로컬 저장소 캐시는 버그와 보안 문제의 원천이 될 수 있음
- 뒤로 가기를 누르면 캐시 대신 서버에서 HTML을 다시 가져옴
- 향후 HTMX 버전에서는 같은 이유로 로컬 저장소 캐싱이 기본 비활성화될 예정임
disableInheritance: true로 HTMX 속성 상속을 비활성화함- 속성을 항상 명시적으로 선언하는 편이 명확함
- 버그나 의도하지 않은 동작의 위험을 낮춤
- 향후 HTMX 버전에서는 속성 상속도 기본 비활성화될 예정임
includeIndicatorStyles: false로 HTMX가 표시기 스타일을 주입하지 않게 함- 표시기 스타일을 다른 CSS 규칙과 함께 직접 정의해 일관성을 유지함
- HTMX 요청은 기본적으로 제한 시간이 없어 서버가 응답할 때까지 기다림
- Go 애플리케이션의 제한 시간과 마감 시각 처리 방식에 따라
timeout을 밀리초 단위로 지정할 수 있음 - 출발점으로
timeout: 5000을 사용함
- Go 애플리케이션의 제한 시간과 마감 시각 처리 방식에 따라
- 기본 구성에는
historyRestoreAsHxRequest: false와 상태 코드별responseHandling도 포함됨
<meta
name="htmx-config"
content='{
"includeIndicatorStyles": false,
"historyCacheSize": 0,
"historyRestoreAsHxRequest": false,
"responseHandling":[
{"code":"204", "swap": false},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": true, "target": "body"},
{"code":"...", "swap": true}
],
"timeout": 5000
}'
>
큰 애플리케이션을 위한 페이지별 레이아웃
- 기본 템플릿과 페이지별 템플릿만으로 부족하다면 두 계층 사이에 레이아웃 템플릿을 추가할 수 있음
- 관리자 페이지처럼 일반 페이지와 다른 제목, 탐색 메뉴, 본문 구조가 필요한 영역에 적합함
base는page:content를 직접 호출하는 대신layout을 호출함
<body>
{{template "layout" .}}
</body>
layouts/admin.tmpl은 관리자 영역의 공통 구조를 정의하고 내부에서page:content를 호출함- 관리자 영역 제목
- 사용자와 주문으로 이동하는 탐색 메뉴
- 페이지별 콘텐츠가 들어갈
<main>
- 핸들러는
render()에 사용할 레이아웃과 페이지 템플릿을 함께 전달함
app.html.render(
w,
200,
nil,
"base",
"layouts/admin.tmpl",
"pages/admin-orders.tmpl",
)
- 공유 기본 템플릿을 유지하면서 경로별 레이아웃과 페이지 콘텐츠를 조합할 수 있어, 관리자 영역처럼 별도 구조가 필요한 화면도 같은 렌더링 패턴으로 확장 가능함
댓글과 토론
Hacker News 의견들
-
Go + HTMX를 좋아하며, 템플릿·컴포넌트·부분 HTML의 타입 안전성을 높이려고 a-h/templ과 함께 사용함
전체 도구 모음을 Go·Unix·SQLite로 구성한 GUS 스택이라 부르며 공개했음: https://housecat.com/blog/the-gus-stack-go-unix-sqlite
exe.dev의 GUTS 스택에서 크게 영감을 받았지만 TypeScript 대신 HTMX를 사용함: https://exe.dev/docs/guts
스택 추적 오류에는 cockroachdb/errors, 타입 안전 HTML에는 templ, OpenAPI 명세 생성에는 fuego, SQL 코드 생성에는 sqlc, 순수 Go SQLite에는 modernc.org/sqlite, 마이그레이션에는 goose, 지속성 워크플로에는 dbos, Chrome/CDP 테스트와 자동화에는 rod를 사용함
이 조합은 직접 코딩하거나 에이전트의 도움을 받아 코딩하고, 단일 바이너리를 빌드·배포하는 과정 모두 생산적임- HTMX를 논할 때 Hyperscript가 마땅한 관심을 받지 못해 아쉬움
서버 왕복이나 별도 JavaScript 함수 없이 DOM을 조작할 수 있어 Go + HTMX와 아주 잘 맞고, 이미 작업 중인 HTML의 확장처럼 느껴짐
선언형 프로그래밍을 좋아하지 않는 사람도 있겠지만, 여러 결제 게이트웨이를 처리하는 Open Payment Host에서 복잡한 DOM 조작까지 Hyperscript만으로 구현할 수 있었음
https://hyperscript.org/, https://openpaymenthost.com/ - HTMX 또는 하이퍼텍스트·Unix·Go·SQLite를 묶어 HUGS 스택으로 이름을 바꿀 수도 있겠음
- Go·Templ·HTMX를 GoTH 스택이라 부르는 경우도 봤고, sqlc까지 더하면 GoTHs나 GHosT가 됨
go generate ./...를 빌드 단계로 받아들이면 얻는 것이 많으며, goverter로 sqlc 모델과 템플릿 객체·반환값 사이의 변환도 생성함
상용구의 약 50%가 자동 생성되어 타입 안전성이 매우 좋아짐 - 최근에는 타입 안전 SQL을 위해 jet을 즐겨 사용함
코드 생성에 실행 중인 데이터베이스가 필요하지만, 코드를 작성하기 전에 마이그레이션부터 적용하도록 강제한다는 점도 장점으로 봄
https://github.com/go-jet/jet - 자체 데이터베이스조차 필요 없을 수 있는 실시간 API 대시보드 같은 소형 반응형 웹 앱에는 최근 PAHG 템플릿을 자주 사용함
Pico.css·Alpine.js·HTMX·Go 조합이며, 물론 SQLite도 연결할 수 있음
https://github.com/hiAndrewQuinn/pahg-template
- HTMX를 논할 때 Hyperscript가 마땅한 관심을 받지 못해 아쉬움
-
HTMX는 여러 용도에 훌륭하지만, 팀 동료가 동의하지 않으면 도입하기 매우 어려움
진지한 기술이 아니다라는 반발이 이어지고 각종 버그가 우선 HTMX 탓으로 돌아가며, 나중에 오해로 밝혀져도 이미 신뢰는 손상됨
지금까지 일한 팀 중 가장 훌륭한 팀에서도 그랬으며, 결국 싸울 대상을 골라야 한다는 교훈을 얻음
Go의 html/template도 인터페이스가 유난히 부자연스럽다고 봄. 《A Philosophy of Software Design》이 권하듯 복잡성을 내부로 밀어 넣어야 하는데, HTML을 렌더링할 때마다 템플릿 복제를 신경 써야 하는 구조는 불필요하게 복잡함- 나도 HTMX를 좋아하지만 팀 도입에서는 같은 현실을 겪음
간단한 웹 앱에 제안했지만, 수년간 JSON을 반환하고 JS/jQuery로 렌더링하던 개발자는 매 단계마다 기존 문제를 새 방식으로 푸는 데 어려움을 겪었고 다른 개발자는 시도조차 하지 않았으며, 팀에서는 계속 Blazor만 거론했음
어떤 새로운 접근법이든 어려워했겠지만 HTMX에는 사실상 기회가 없었음
소규모 팀이라 JavaScript의 95%를 피하고 서버에서 코드와 HTML을 생성하면 코드베이스가 더 정돈되고 테스트도 쉬워질 텐데 아쉬움. 개인 프로젝트에서는 계속 HTMX를 사용하며 좋은 흐름을 찾았고 웹 개발이 크게 단순해졌다고 느낌 - 자문 역할에서 팀을 적극적으로 지도하며, HTMX를 배우면 실제 작업 환경인 브라우저에 대한 이해가 깊어지는 반면 React 등은 브라우저로부터 개발자를 격리한다고 설명했지만 나도 같은 일을 겪음: https://www.reddit.com/r/htmx/s/DuXyGgsCWK
html/template은 기본 보안과 표준 내장이라는 점이 좋지만 동적 특성 때문에 답이 정해지지 않은 부분이 너무 많고, Templ도 대규모 상용 앱을 몇 개 작성해 보니 개발 편의성이 아쉬웠음
그래서 gsx를 만들었음: https://github.com/gsxhq/gsx - 템플릿을 렌더링할 때마다 복제할 필요는 없으며, 한 번 컴파일한 뒤 실행만 하면 됨
원문의 문제는 주 템플릿에서 페이지 제목을 지정하려 하기 때문에 스스로 만든 것임. 제목을 문맥으로 전달하고base.tmpl을BASE_BEGIN과BASE_END로 나눠 최종 템플릿에서 사용하면 됨
페이지별 제목을 실행 시 전달해야 하지만, 원문의 방식도 다국어 지원을 시작하면 결국 깨짐 - 우리 회사에서는 정반대로, 온갖 문제가 있어도 React SPA가 확립된 기술이라는 이유로 확장되지 않는다는 사실을 설득하기가 불가능함
각 문제는 개별적으로 보면서도 모두 같은 구조에서 비롯됐다는 큰 그림으로 연결하지 못함
- 나도 HTMX를 좋아하지만 팀 도입에서는 같은 현실을 겪음
-
최근 프로젝트에서 HTMX를 매우 즐겁게 사용함
AngularJS와 React 이전의 웹을 아는 입장에서 실제 페이지를 만들고 JavaScript 양을 최소화할 수 있다는 점이 특히 좋음
순수 JavaScript로도 가능하지만 HTMX는 반복되는 이벤트 처리 상용구를 대신해 줌
현대 프런트엔드 철학을 좋아하지 않는다면 시도해 볼 만하며, 공식 사이트의 첫 예제들은 아주 기본적이지만 조금 더 배우면 훨씬 강력함- 조심스럽게 SvelteKit도 권하고 싶음
겉으로는 또 하나의 JavaScript 프런트엔드 프레임워크처럼 보이지만, 직접 사용해 보니 HTMX 계열이 추구하는 하이퍼텍스트 중심 설계를 훌륭하게 지원함
- 조심스럽게 SvelteKit도 권하고 싶음
-
상당히 큰 프로젝트를 HTMX + Go로 만들어 봤지만 아직 충분하지 않았고, 앞으로도 원하는 수준에 이를지는 확신하지 못함
단순 CRUD 앱과 관리 대시보드에는 훌륭하지만 서로 연결된 컴포넌트, 공유 상태, 복잡한 상호작용이 많아지면 관리가 빠르게 어려워짐
React가 싫어 HTMX를 골랐다가 결국 백엔드는 Go로 유지하고 프런트엔드는 SvelteKit SPA 모드로 바꿨으며, 양쪽을 깔끔히 분리하면서 복잡한 UI를 훨씬 쉽게 개발·유지할 수 있었음
Svelte는 JSX라는 다른 언어보다 HTML의 자연스러운 확장처럼 느껴지고, 상태 관리와 컴포넌트 모델이 단순하며 새$state문법도 특히 좋음- HTMX 사이트도 그런 용도에는 맞지 않는다는 절충점을 명확히 밝히고 있지만, 관련 글을 찾으려면 다소 깊이 들어가야 함
- HTMX 개발진의 프런트엔드 형제 프로젝트인 Hyperscript는 프런트엔드 상태 같은 기능을 처리함. 이것도 사용해 봤는지 궁금함: https://hyperscript.org/docs/reactivity/
- HTMX 철학의 핵심은 애초에 공유 상태로 연결된 컴포넌트가 정말 필요한지 질문하는 데 있지 않나 싶음
결국 대부분의 앱은 데이터베이스를 대상으로 CRUD를 수행함
-
요즘 사이드 프로젝트는 LLM의 도움 여부와 관계없이 거의 모두 Go + HTMX로 작성함
Opus와 GPT도 이 조합을 아주 잘 다루며, 빠르게 만들고 시작할 수 있고 단일 바이너리라 배포와 호스팅도 편리함
빠른 반복 개발에 매우 좋은 스택임 -
HTMX를 쓴다면 백엔드에서도 React처럼 공통 HTML 조각을 함수로 추출해 컴포넌트화하기 쉬운 HTML 생성 방식을 강력히 권함
HTMX는 상황에 따라 특정 HTML 바로 옆에 태그를 붙이거나 빼야 하는 등 백엔드가 생성하는 HTML에 유연성이 필요함. 예를 들어 최상위에서<title>태그를 찾으면 페이지 제목을 갱신함
전통적인 문자열 기반 템플릿 엔진에서는 이런 작업이 어렵지만 언어 내장형 HTML 라이브러리에서는 간단하며, 언어 전체의 추상화 기능을 활용할 수 있어 HTMX와 특히 잘 맞음
JavaScript 백엔드라면 <a href="https://github.com/WebReflection/uhtml-ssr" rel="nofollow">https://github.com/WebReflection/uhtml-ssr</a> 같은 태그드 템플릿 리터럴 함수, Go라면 <a href="https://www.gomponents.com/" rel="nofollow">https://www.gomponents.com/</a>, Scala라면 ScalaTags를 사용할 수 있음- Python 백엔드에서는 JinjaX를 성공적으로 사용하고 있음: https://jinjax.scaletti.dev/
- uhtml-ssr은 조금 오래돼 보임
의존성 없는 HTML 태그드 템플릿 리터럴 라이브러리를 원한다면 다음 구현과 문서를 참고할 만함
구현: https://github.com/mastrojs/mastro/blob/main/src/core/html.t...
문서: https://mastrojs.github.io/docs/html-components/ - 백엔드에서도 JSX를 사용할 수 있음
React에는renderToStaticMarkup가 있지만, 백엔드 전용으로 작성된 JSX 라이브러리를 선택하는 편이 나을 것임
-
Alex Edwards의 책과 Learn Go with Tests로 Go를 처음 접했으며 지금도 추천함
오래된 프로젝트 일부를 HTMX로 전환해 보고 싶어짐- Go 웹 개발에 조금이라도 관심이 있다면 Alex Edwards의 Let's Go와 Let's Go Further를 모두 읽어야 함
지금까지 읽은 프로그래밍 언어 입문서 중 가장 접근하기 쉽고 즐거우며 실용적인 책들임
- Go 웹 개발에 조금이라도 관심이 있다면 Alex Edwards의 Let's Go와 Let's Go Further를 모두 읽어야 함
-
Kotlin + HTMX 기반 프레임워크를 개인 용도로 만들었음: https://github.com/reubenfirmin/zoned
웹 앱 전체를 종단 간 타입 안전하게 만들 수 있는지 확인하는 것이 목표였고, JSX와 비슷한 DSL로 HTML을 작성하는 Kotlinx.html을 사용함
처음 90%는 직접 만들고 마지막 10%와 README는 공개를 마무리하려고 Claude의 도움을 받음 -
Go와 Alex Edwards 모두 좋아하지만 HTMX를 사용한 결과는 늘 실망스러웠음
코드베이스 복잡도가 앱 자체보다 2배 빠르게 늘어나는 느낌이고, 이상한 편법 없이는 빠져나올 수 없는 경계 조건을 항상 만나게 됨
Node 패키지를 싫어하는 이유는 이해하지만 HTMX는 그에 대한 과잉 보상처럼 보임. JSON과 씨름하지 않아 절약한 시간보다 앱의 외관과 사용감을 제대로 만드는 데 드는 시간이 3배 더 큼
Mantine 템플릿은 2분이면 구성해 뛰어난 UI 컴포넌트를 활용할 수 있고, 빌드된 정적 자산을 포함하면 똑같이 단일 Go 바이너리로 만들 수 있음: https://github.com/mantinedev/vite-min-template- Rails로 하이퍼미디어를 처음 접했을 때는 편리하고 쉬웠기에 Go/HTMX의 마찰이 의외였지만, 여러 번 시도하며 추상화가 중요하다는 점을 배움
컴포넌트 생성기와 역방향 라우터가 없으면 상당히 고통스러우며 Rails는 이를 기본 제공해서 신경 쓸 필요가 없었음
가장 큰 장점은 계산과 데이터가 한곳에 있다는 점임. LLM도 자체 완결적인 하이퍼미디어 앱을 잘 사용하고 테스트하며, JavaScript를 기다리지 않아 반복 주기가 짧아짐
다만 바로 가져다 쓸 컴포넌트 라이브러리가 없어서 시작하기 어렵다는 데는 동의함 - HTMX를 사용하려면 설계 철학 전체를 바꿔야 했음
반드시 단점이라기보다 좋은 변화일 수도 있지만 상당한 전환이며, 결과물은 모바일 앱을 흉내 내기보다 웹 고유의 형태에 가까워짐 - Mantine으로는 만들 수 있지만 HTMX로는 만들 수 없었던 UI의 구체적 예시가 무엇인지 궁금함
- Rails로 하이퍼미디어를 처음 접했을 때는 편리하고 쉬웠기에 Go/HTMX의 마찰이 의외였지만, 여러 번 시도하며 추상화가 중요하다는 점을 배움
-
Go + Datastar가 더 단순해서 훨씬 선호함
- Go와 Datastar는 맞춤 장갑처럼 잘 어울림
템플릿에는 a-h/templ을 추가하고 싶은데 Datastar도 지원함
Datastar가 이토록 덜 알려졌다는 사실이 놀라움 - 나도 Datastar를 선호하지만 HTMX4의 새 기능이 격차를 많이 좁히고 있음
생태계가 좋은 패턴으로 수렴하는 모습은 반가움
- Go와 Datastar는 맞춤 장갑처럼 잘 어울림