# Go에서 HTMX를 활용하는 방법

> Clean Markdown view of GeekNews topic #31469. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=31469](https://news.hada.io/topic?id=31469)
- GeekNews Markdown: [https://news.hada.io/topic/31469.md](https://news.hada.io/topic/31469.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-16T06:42:19+09:00
- Updated: 2026-07-16T06:42:19+09:00
- Original source: [alexedwards.net](https://www.alexedwards.net/blog/how-i-use-htmx-with-go)
- Points: 1
- Comments: 1

## Topic Body

- 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 렌더러 코드
- 기본 골격은 다음 명령으로 생성함

```sh
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`을 사용함

```sh
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`은 `&lt;head&gt;`, 공통 제목, `&lt;main&gt;` 등 전체 문서 골격을 정의하고 `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 조각 렌더링
- 홈페이지 핸들러는 다음 호출로 전체 문서를 반환함

```go
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
```

- 공유 집합에 `pages/home.tmpl`을 추가하고 `base` 템플릿을 `200 OK`로 실행하는 호출임
- `/gopher` 핸들러는 추가 파일을 파싱하지 않고, 공유 집합에 이미 포함된 **`partial:image:gopher`** 만 실행함

```go
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"`로 응답을 `&lt;tbody id="search-results"&gt;` 내부에 넣음
  - `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`** 헤더가 포함되므로 이를 기준으로 응답 형태를 결정함

```go
func isHTMXRequest(r *http.Request) bool {
    return r.Header.Get("HX-Request") == "true"
}
```

- 검색 핸들러의 기본 실행 템플릿을 `base`로 두고, HTMX 요청일 때만 `users:rows`로 변경함
  - 직접 방문하면 검색 결과가 포함된 전체 페이지를 받음
  - HTMX 요청에는 `&lt;tbody&gt;`에 넣을 행 조각만 반환함

### `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 응답을 유지해야 함

```go
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`로 이동할 때는 다음처럼 호출할 수 있음

```go
redirect(w, r, "/profile", http.StatusSeeOther)
```

### `HX-Redirect`와 `HX-Location`의 차이
- `HX-Location`은 전체 페이지를 다시 불러오지 않고 리다이렉트와 유사한 동작을 수행함
  - 대상 URL의 HTML을 가져옴
  - 응답을 `&lt;body&gt;`에 교체함
  - 브라우저 기록에 새 항목을 추가함
- 대상 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 응답은 **`&lt;body&gt;` 전체에 교체**하도록 설정함
- 예외는 `422 Unprocessable Content`임
  - 폼 검증 오류가 포함된 폼을 반환할 때 사용함
  - 기존 HTMX 대상에 응답이 정상적으로 교체돼야 함
- `responseHandling`은 상태 코드에 따라 다음처럼 구성함
  - `204 No Content`: DOM을 변경하지 않음
  - `422 Unprocessable Content`: 기존 대상에 정상 교체함
  - 나머지 4xx와 5xx: 응답을 `&lt;body&gt;`에 교체함
  - 그 밖의 응답: 기존 대상에 정상 교체함

```json
"responseHandling":[
    {"code":"204", "swap": false},
    {"code":"422", "swap": true},
    {"code":"[45]..", "swap": true, "target": "body"},
    {"code":"...", "swap": true}
]
```

- 특정 상호작용에서 오류를 `&lt;body&gt;`가 아닌 다른 대상에 넣어야 한다면 **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`을 반환함

```go
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`** 을 사용함
- 기본 구성에는 `historyRestoreAsHxRequest: false`와 상태 코드별 `responseHandling`도 포함됨

```html
<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`을 호출함

```gotemplate
&lt;body&gt;
    {{template "layout" .}}
&lt;/body&gt;
```

- `layouts/admin.tmpl`은 관리자 영역의 공통 구조를 정의하고 내부에서 `page:content`를 호출함
  - 관리자 영역 제목
  - 사용자와 주문으로 이동하는 탐색 메뉴
  - 페이지별 콘텐츠가 들어갈 `&lt;main&gt;`
- 핸들러는 `render()`에 사용할 레이아웃과 페이지 템플릿을 함께 전달함

```go
app.html.render(
    w,
    200,
    nil,
    "base",
    "layouts/admin.tmpl",
    "pages/admin-orders.tmpl",
)
```

- 공유 기본 템플릿을 유지하면서 경로별 레이아웃과 페이지 콘텐츠를 조합할 수 있어, 관리자 영역처럼 별도 구조가 필요한 화면도 같은 렌더링 패턴으로 확장 가능함

## Comments



### Comment 61878

- Author: neo
- Created: 2026-07-16T06:42:20+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48912175) 
- **Go + HTMX**를 좋아하며, 템플릿·컴포넌트·부분 HTML의 타입 안전성을 높이려고 a-h/templ과 함께 사용함  
  전체 도구 모음을 Go·Unix·SQLite로 구성한 **GUS 스택**이라 부르며 공개했음: [https://housecat.com/blog/the-gus-stack-go-unix-sqlite](<https://housecat.com/blog/the-gus-stack-go-unix-sqlite>)  
  exe.dev의 GUTS 스택에서 크게 영감을 받았지만 TypeScript 대신 HTMX를 사용함: [https://exe.dev/docs/guts](<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://hyperscript.org/>), [https://openpaymenthost.com/](<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](<https://github.com/go-jet/jet>)
  - 자체 데이터베이스조차 필요 없을 수 있는 실시간 API 대시보드 같은 소형 반응형 웹 앱에는 최근 **PAHG 템플릿**을 자주 사용함  
    Pico.css·Alpine.js·HTMX·Go 조합이며, 물론 SQLite도 연결할 수 있음  
    [https://github.com/hiAndrewQuinn/pahg-template](<https://github.com/hiAndrewQuinn/pahg-template>)

- 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](<https://www.reddit.com/r/htmx/s/DuXyGgsCWK>)  
    html/template은 기본 보안과 표준 내장이라는 점이 좋지만 동적 특성 때문에 답이 정해지지 않은 부분이 너무 많고, Templ도 대규모 상용 앱을 몇 개 작성해 보니 개발 편의성이 아쉬웠음  
    그래서 **gsx**를 만들었음: [https://github.com/gsxhq/gsx](<https://github.com/gsxhq/gsx>)
  - 템플릿을 렌더링할 때마다 복제할 필요는 없으며, 한 번 컴파일한 뒤 실행만 하면 됨  
    원문의 문제는 주 템플릿에서 페이지 제목을 지정하려 하기 때문에 스스로 만든 것임. 제목을 문맥으로 전달하고 `base.tmpl`을 `BASE_BEGIN`과 `BASE_END`로 나눠 최종 템플릿에서 사용하면 됨  
    페이지별 제목을 실행 시 전달해야 하지만, 원문의 방식도 **다국어 지원**을 시작하면 결국 깨짐
  - 우리 회사에서는 정반대로, 온갖 문제가 있어도 **React SPA**가 확립된 기술이라는 이유로 확장되지 않는다는 사실을 설득하기가 불가능함  
    각 문제는 개별적으로 보면서도 모두 같은 구조에서 비롯됐다는 큰 그림으로 연결하지 못함

- 최근 프로젝트에서 **HTMX**를 매우 즐겁게 사용함  
  AngularJS와 React 이전의 웹을 아는 입장에서 실제 페이지를 만들고 JavaScript 양을 최소화할 수 있다는 점이 특히 좋음  
  순수 JavaScript로도 가능하지만 HTMX는 반복되는 이벤트 처리 상용구를 대신해 줌  
  현대 프런트엔드 철학을 좋아하지 않는다면 시도해 볼 만하며, 공식 사이트의 첫 예제들은 아주 기본적이지만 조금 더 배우면 훨씬 강력함
  - 조심스럽게 **SvelteKit**도 권하고 싶음  
    겉으로는 또 하나의 JavaScript 프런트엔드 프레임워크처럼 보이지만, 직접 사용해 보니 HTMX 계열이 추구하는 하이퍼텍스트 중심 설계를 훌륭하게 지원함

- 상당히 큰 프로젝트를 **HTMX + Go**로 만들어 봤지만 아직 충분하지 않았고, 앞으로도 원하는 수준에 이를지는 확신하지 못함  
  단순 CRUD 앱과 관리 대시보드에는 훌륭하지만 서로 연결된 컴포넌트, 공유 상태, 복잡한 상호작용이 많아지면 관리가 빠르게 어려워짐  
  React가 싫어 HTMX를 골랐다가 결국 백엔드는 Go로 유지하고 프런트엔드는 **SvelteKit SPA 모드**로 바꿨으며, 양쪽을 깔끔히 분리하면서 복잡한 UI를 훨씬 쉽게 개발·유지할 수 있었음  
  Svelte는 JSX라는 다른 언어보다 HTML의 자연스러운 확장처럼 느껴지고, 상태 관리와 컴포넌트 모델이 단순하며 새 `$state` 문법도 특히 좋음
  - HTMX 사이트도 그런 용도에는 맞지 않는다는 **절충점**을 명확히 밝히고 있지만, 관련 글을 찾으려면 다소 깊이 들어가야 함
  - HTMX 개발진의 프런트엔드 형제 프로젝트인 **Hyperscript**는 프런트엔드 상태 같은 기능을 처리함. 이것도 사용해 봤는지 궁금함: [https://hyperscript.org/docs/reactivity/](<https://hyperscript.org/docs/reactivity/>)
  - HTMX 철학의 핵심은 애초에 **공유 상태로 연결된 컴포넌트**가 정말 필요한지 질문하는 데 있지 않나 싶음  
    결국 대부분의 앱은 데이터베이스를 대상으로 CRUD를 수행함

- 요즘 사이드 프로젝트는 LLM의 도움 여부와 관계없이 거의 모두 **Go + HTMX**로 작성함  
  Opus와 GPT도 이 조합을 아주 잘 다루며, 빠르게 만들고 시작할 수 있고 단일 바이너리라 배포와 호스팅도 편리함  
  빠른 반복 개발에 매우 좋은 스택임

- HTMX를 쓴다면 백엔드에서도 React처럼 공통 HTML 조각을 함수로 추출해 **컴포넌트화**하기 쉬운 HTML 생성 방식을 강력히 권함  
  HTMX는 상황에 따라 특정 HTML 바로 옆에 태그를 붙이거나 빼야 하는 등 백엔드가 생성하는 HTML에 유연성이 필요함. 예를 들어 최상위에서 `&lt;title&gt;` 태그를 찾으면 페이지 제목을 갱신함  
  전통적인 문자열 기반 템플릿 엔진에서는 이런 작업이 어렵지만 **언어 내장형 HTML 라이브러리**에서는 간단하며, 언어 전체의 추상화 기능을 활용할 수 있어 HTMX와 특히 잘 맞음  
  JavaScript 백엔드라면 &lt;a href="https://github.com/WebReflection/uhtml-ssr" rel="nofollow"&gt;https://github.com/WebReflection/uhtml-ssr&lt;/a&gt; 같은 태그드 템플릿 리터럴 함수, Go라면 &lt;a href="https://www.gomponents.com/" rel="nofollow"&gt;https://www.gomponents.com/&lt;/a&gt;, Scala라면 ScalaTags를 사용할 수 있음
  - Python 백엔드에서는 **JinjaX**를 성공적으로 사용하고 있음: [https://jinjax.scaletti.dev/](<https://jinjax.scaletti.dev/>)
  - uhtml-ssr은 조금 오래돼 보임  
    의존성 없는 HTML 태그드 템플릿 리터럴 라이브러리를 원한다면 다음 구현과 문서를 참고할 만함  
    구현: [https://github.com/mastrojs/mastro/blob/main/src/core/html.t...](<https://github.com/mastrojs/mastro/blob/main/src/core/html.ts>)  
    문서: [https://mastrojs.github.io/docs/html-components/](<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**를 모두 읽어야 함  
    지금까지 읽은 프로그래밍 언어 입문서 중 가장 접근하기 쉽고 즐거우며 실용적인 책들임

- **Kotlin + HTMX** 기반 프레임워크를 개인 용도로 만들었음: [https://github.com/reubenfirmin/zoned](<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](<https://github.com/mantinedev/vite-min-template>)
  - Rails로 하이퍼미디어를 처음 접했을 때는 편리하고 쉬웠기에 Go/HTMX의 마찰이 의외였지만, 여러 번 시도하며 **추상화가 중요하다**는 점을 배움  
    컴포넌트 생성기와 역방향 라우터가 없으면 상당히 고통스러우며 Rails는 이를 기본 제공해서 신경 쓸 필요가 없었음  
    가장 큰 장점은 계산과 데이터가 한곳에 있다는 점임. LLM도 자체 완결적인 하이퍼미디어 앱을 잘 사용하고 테스트하며, JavaScript를 기다리지 않아 반복 주기가 짧아짐  
    다만 바로 가져다 쓸 컴포넌트 라이브러리가 없어서 시작하기 어렵다는 데는 동의함
  - HTMX를 사용하려면 **설계 철학 전체**를 바꿔야 했음  
    반드시 단점이라기보다 좋은 변화일 수도 있지만 상당한 전환이며, 결과물은 모바일 앱을 흉내 내기보다 웹 고유의 형태에 가까워짐
  - Mantine으로는 만들 수 있지만 HTMX로는 만들 수 없었던 **UI의 구체적 예시**가 무엇인지 궁금함

- **Go + Datastar**가 더 단순해서 훨씬 선호함
  - Go와 Datastar는 맞춤 장갑처럼 잘 어울림  
    템플릿에는 a-h/templ을 추가하고 싶은데 Datastar도 지원함  
    **Datastar**가 이토록 덜 알려졌다는 사실이 놀라움
  - 나도 Datastar를 선호하지만 **HTMX4의 새 기능**이 격차를 많이 좁히고 있음  
    생태계가 좋은 패턴으로 수렴하는 모습은 반가움
