

원인 파악이 쉽지 않은 버그를 찾느라 코드를 한 줄씩 따라가고, breakpoint를 찍으며 고생해 본 경험이 있으신가요? 오늘의집에서는 여러 MSA Aggregation 서버를 통합하는 SuperRoot를 운영하고 있습니다. Superoot는 그 특성상 비교적 일관된 처리 패턴을 가지고 있습니다.
참고 글 : 오늘의집 Serving Platform을 통해 비효율성 해소하기
Serving Platform이 성장하며 연관된 서비스와 비즈니스 로직을 처리하기 위한 PageFilter 도 점점 복잡해졌습니다. 이로 인해 문제가 발생했을 때 원인을 파악하기가 점점 어려워졌습니다.

이런 상황에서 기존처럼 API를 직접 호출해 결과를 확인하고, 의심되는 지점마다 로그를 찍어가면서 디버깅하는 방식은 점점 비효율적으로 느껴졌습니다. 반복되는 디버깅의 부담을 줄이고 이 과정을 근본적으로 개선할 방법이 필요하다고 생각했습니다.
특히 Superroot가 일관된 처리 패턴 을 갖고 있기 때문에, 이를 시각화할 수 있다면 개발자가 머릿속에서 변화를 상상하면서 겪는 인지부하를 크게 줄일 수 있을 거라 판단했습니다. 그래서 다음과 같은 정보들을 한눈에 볼 수 있는 웹 기반 UI를 만들어보기로 했습니다.
- 페이지 요청 설정
- 모듈 요청/응답
- PageFilter 적용 순서
- PageFilter 전/후 비교
처음에는 단순한 정적 UI도 고려했지만, 모듈 요청/응답이나 PageFilter 전/후 비교처럼 데이터가 큰 영역은 클릭 시 부분적인 화면갱신이 가능하다면 훨씬 편할 것으로 판단했습니다. 이 과정에서 React/Vue 같은 정통 프론트엔드 프레임워크의 사용도 고민했지만, 다음과 같은 이유로 적합하지 않다고 느꼈습니다.
- npm / webpack 등 사용하는 빌드 프로세스가 존재
- JavaScript + 프레임워크 학습 필요
더 나은 방법을 찾아보던 중, HTMX를 발견했습니다.
HTMX 소개

HTMX는 HTML 속성만으로 Ajax 요청 / DOM 업데이트 등을 할 수 있는 JavaScript 라이브러리입니다.
<!-- head에 넣기만 하면 설치 완료 -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<!-- 아래 버튼을 누르면 AJAX 요청 결과로 내용이 교체됩니다. -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
</button>
위 코드에서 버튼을 클릭하면 /clicked 경로로 요청을 보내고, 서버 응답으로 들어온 HTML이 기존 버튼을 대체합니다. 그렇다면 왜 HTMX 일까요?
1. 경량성 & 빌드 필요 없음
HTMX는 약 16KB에 불과한 매우 가벼운 라이브러리입니다. 무엇보다 중요한 점은 npm, webpack 같은 빌드 과정 없이 단순히 서버에서 내려주는 HTML에 htmx 스크립트만 포함하면 바로 사용할 수 있습니다.
2. JavaScript 사용을 최소화

이번에 만들고자 한 웹 UI는 다음과 같은 기능만을 필요로 했습니다.
- 인증이나 토큰 갱신 같은 클라이언트 로직 필요 없음
- 복잡한 클라이언트 상태 관리 필요 없음
- 대부분의 화면이 서버에서 계산된 결과를 그대로 보여주면 충분함
결국 서버에서 HTML 조각을 내려받아 특정 영역만 갱신하는 것만으로 충분했습니다. 그렇기에 거대한 프론트엔드 프레임워크를 다루기 보다는, 서버 개발 중심으로 상호작용에 필요한 부분만 HTMX를 사용하고자 했습니다.
3. kotlin과의 조화
✔️ kotlinx.html
상기했던 것처럼, HTMX를 사용하면 서버에서는 JSON을 내려줄 필요 없이 HTML을 직접 내려주면 됩니다. Superroot 서버는 Kotlin을 사용하고 있는데요. kotlinx.html 라는 라이브러리를 사용하면 HTML을 아름답고 타입 안전한 DSL으로 내려줄 수 있습니다.
✔️ webjars
Kotlin은 JVM 기반 언어이기에 WebJars로 HTMX를 패키지에 포함할 수 있습니다. 서버에서 버전을 명시적으로 고정하며, 외부 CDN에 의존하지 않고 빌드 아티팩트에 포함되어 관리할 수 있다는 장점이 있습니다.
HTMX 구현 예제
앞서 이야기한 장점들이 실제 코드에서 어떻게 드러나는지 간단한 예시를 통해 살펴보겠습니다.
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
implementation("org.webjars:webjars-locator-core")
implementation("org.webjars.npm:htmx.org:2.0.3")
}
필요한 라이브러리를 추가한 뒤, 다음과 같이 컨트롤러를 구성했습니다.
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.springframework.web.bind.annotation.*
@RestController
class WebController {
@GetMapping("/")
fun index(): String = createHTML().html {
head {
// webjars 통해서 htmx import
script(src = "/webjars/htmx.org/2.0.3/dist/htmx.min.js") {}
}
body {
button {
// htmx 속성 설정
attributes["hx-get"] = "/time"
attributes["hx-target"] = "#result"
attributes["hx-swap"] = "innerHTML"
+"Load Time"
}
div {
id = "result"
+"(press button)"
}
}
}
// 서버에서 주는 응답을 이렇게 HTML DSL 로 내려주면 됩니다.
@GetMapping("/time")
fun timeFragment(): String = createHTML().div {
+"Server time: ${java.time.LocalTime.now()}"
}
}
이렇게 DSL 기반의 간결한 문법으로 직접 HTML을 내려줄 수 있습니다. 버튼 클릭 시 /time의 HTML 조각이 #result 영역에 갱신되는걸 볼 수 있습니다.

즉, JavaScript 코드가 거의 없는 상태에서 HTML 속성만으로 필요한 상호작용을 구현했습니다.
쇼케이스
이후 기능을 확장하고 다듬어 UI를 최종적으로 완성했습니다. 이 과정 속에서 좀 더 완성도 높은 UI를 만들기 위해 HTMX와 궁합이 좋은 몇 가지 경량 라이브러리를 더했습니다.
- pico.css - 스타일링
- jsondiffpatch - JSON diff
디버그 모드로 필요한 요청을 편하게 설정하고, 각 MSA 모듈별 응답 상태 및 소요 시간을 한눈에 확인할 수 있습니다.

rerank / enrichment 등 핵심 비즈니스 로직이 적용되는 PageFilter의 전후 변화를 시각적으로 비교할 수 있습니다.

마치며
이제는 버그를 찾을 때 골머리를 싸매는 시간이 확실히 줄어들었습니다. 또한, 단순한 디버깅을 넘어서 SuperRoot에서 개발할 때에 이 UI를 띄우는 것이 기본 과정이 되었습니다. JavaScript 대신 Kotlin으로 HTML을 작성할 수 있다는 점도 인상적인 경험이었습니다.
Discovery Platform팀은 앞으로도 서비스 흐름을 더 잘 이해하고 안정적으로 개선할 수 있는 개발 환경을 지속적으로 고민해 나갈 예정입니다. 읽어주셔서 감사합니다!
