나 혼자 다해먹을 htmx

Betalabs·2023년 4월 10일
20
post-thumbnail

안녕하세요!
Betalabs의 Soora입니다.

개발에서 은탄환은 없지만, 많은 기술을 알고 있으면 선택지가 넓어져서 더 좋은 선택을 할 수 있다고 생각합니다.
오늘은 서버 기술보다는 프론트 영역에 조금 더 가까운 htmx에 대해서 이야기해보겠습니다.

들어가기 앞서

과거를 회상하며

요새는 많은 회사가 전문적으로 프로젝트를 진행하기 위해 프론트팀과 백엔드 팀을 나눠 업무를 진행합니다. 하지만 제가 처음 개발을 시작했던 회사에는 프론트 팀은 없었고, 퍼블리셔가 틀이 되는 HTMLCSS를 작성하여 백엔드팀에 전달해주면 JSPThymeleaf를 사용하여 MPAWeb Application을 개발했습니다. MPA로 구현된 Web Application에서는 사용자가 액션을 하면 Page가 이동되며 약간의 불편함이 있었습니다. 하지만 그때도 SPA 기반의 Web Application을 제공했습니다. 하지만 조금 더 많은 시간이 흐른 뒤 사용자의 편리함이 중요시됨과 동시에 SPA이 더 대중화가 되어 가는듯한 느낌을 받았습니다. 요새는 MPA보다 SPA을 더 많이 경험하고 있는 것 같습니다.

MPA

MPAMulti Page Application을 이야기합니다. 이름에서 알 수 있듯이 하나의 Page만을 통해 Web Application을 구성하는 게 아닌, 여러 Page를 통해 Web Application을 구현합니다. 사용자가 LinkForm 을 통한 Data를 전송하면 새로운 Page로 이동이 됩니다. 이미 우리는 이러한 경험을 많이 해봤고, 최근에도 구글 주 페이지에서 검색을 하면 이런 경험을 할 수 있습니다. 이런 전통적인 MPA를 사용하면 각 Page마다 HTML을 포함한 정적 리소스를 받아오며, Page 전체를 다시 Rendering 하는 과정을 통해 불편한 경험을 느낄 수 있습니다. 대신 MPA는 백엔드 개발팀만 있어도 개발이 가능할 정도로 손쉽게 접근을 할 수 있습니다.

MPA를 통해 웹 애플리케이션을 구현한다면 Spring과 함께 JSP, Freemarker, Thymeleaf를 통해서 구현할 수 있습니다.

SPA

SPASingle Page Application을 이야기합니다. SPAWeb Application 전체를 하나의 Page에서 제공이 되며, LinkForm을 통한 Data를 전송하면 새로운 Page로 이동되는 것이 아닌, 최초 RenderingPage에서 필요한 부분만 새롭게 Rendering을 합니다. 이러한 특징으로 MPA와 다르게 최초의 Page에서 다시 Rendering을 하므로 사용자의 경험을 향상할 수 있습니다. 이미 SPA는 많은 Web Application에서 경험하실 수 있으며, FaceBook이나 Instagram에서 새로운 Feed를 가져오는 기능과 Velog도 SPA를 통해 Web Application를 제공하고 있는 것으로 확인이 됩니다.
하지만 SPA는 전문적인 팀을 구성할 정도로 초기 구성이 복잡하고, 러닝커브가 MPA보다 높습니다.

SPA를 통해 Web Application을 구현한다면 React, Angular, Vue.js 등을 통해 구현할 수 있습니다.

문제

무엇이?


이미 팀 빌딩이 된 상태로 프로젝트를 진행하면 백엔드 팀은 프론트 팀에게 API에 대한 제약 조건과 예외 상황 등의 도메인 지식과 API 스펙 문서를 작성하여 전달 합니다. 처음부터 이 모든 게 완벽했으면 좋겠지만, 실수 또는 요구사항의 변경으로 다시 공유하는 상황이 발생합니다. 또 다른 문제로는 백엔드 팀의 실수나 프론트 팀의 실수로 스펙에 맞지 않는 요청/응답이 발생한 경우 모두가 다 같이 모여서 디버깅을 진행합니다. 결국 문제를 해결하기 위해서는 백엔드 팀과 프론트 팀이 옹기종기 모여서 디버깅하고, 모두의 리소스를 소모하게 됩니다. 팀이 이미 잘 구성된 팀은 그나마 행복해 보일 수 있어 보입니다. 만약 백엔드 팀만으로 상품을 출시해야 하는 스타트업의 경우, 사용자의 경험을 향상하기 위해서 Page 이동이 없는 동적 Rendering을 구현해야 하는데 SPA의 높은 러닝 커브를 극복하기는 쉽지 않을 것입니다.

고민하기

문제가 되는 API 스펙 문서의 경우 asciidoc과 같은 문서화를 통해 코드와 문서를 일치하게 했고, 문서가 변경되면 CI를 할 때 이미 Slack을 통해 알림을 주는 방식으로 해결할 수 있습니다.

하지만 다른 문제들은 소프트웨어적으로 해결하기는 쉽지 않아 보이고, 해결하기 위해서는 한 사람(또는 한 팀)이 해결할 수 있도록 응집시키는 방법을 생각해볼 수 있으나, 백엔드 개발자가 Vue.jsReact를 공부하기는 쉽지 않고, 프론트 개발자가 SpringDB를 공부하기는 더더욱 쉽지 않아 보입니다. 또 다른 방법으로는 초심으로 돌아가서 백엔드 팀이 SSR를 하여 전통적인 MPA으로 Web Application을 구현한다면, 사용자 경험 향상은 어렵고 불편하게만 느껴질 것입니다.

htmx

아이디어

만약 Data를 가지고 Rendering을 하는 게 아닌, 이미 RenderingPage를 받아서 필요한 부분에 넣어준다면 MPASPA의 장점을 잘 활용할 수 있을 것입니다. 하지만 Ajax를 사용하여 이런 기능을 하는 Javascript를 직접 개발, 문서화, 유지보수하는 것은 더 쉽지 않을 수 있습니다.
만약 이런 기능을 해주는 Library가 있다면 백엔드 팀 혼자서도 어느 정도 수준의 사용자 경험 향상된 웹 애플리케이션을 구현할 수 있지 않을까요?

htmx가 뭔데?

htmx(https://htmx.org/)는 HTML을 사용하여 동적으로 PageRendering할 수 있도록하는 Javascript Library 입니다.htmxHTML 태그에 정의된 속성을 추가하여 서버와의 통신을 할 수 있도록 하며, 서버로부터 HTML을 받아서 정의된 속성을 기반으로 필요한 부분만 새롭게 Rendering을 할 수 있도록 도와줍니다. 결국 HTML 태그에 정의된 속성만을 사용하여 Rendering을 할 수 있으니, Javascript 없이 HTML 만으로도 SPA의 특징을 쉽고 간단하게 구현할 수 있습니다.

htmxSPA의 특징을 가지고 있지만, SPA는 서버로부터 Page를 받는 게 아닌, Data를 받아서 최초의 Page에서 Rendering을 하는 것을 의미합니다. 하지만 htmx는 서버에서 Page를 받고 Rendering을 하기에 SPA라고 부르기는 다소 어려운 부분이 있습니다.

실전

설치

htmxJavascript Library이므로, 원하는 곳에 다음과 같이 CDN을 통해서 Javascript를 받아오는 코드를 넣어주면 됩니다. 필요에 따라서는 다운로드 후 프로젝트에 넣어주시면 됩니다.

<script src="https://unpkg.com/htmx.org@1.8.6"></script>

사용법

원하는 태그에 htmx의 속성을 넣으면 Javascript 없이 서버에 요청 및 Rendering이 됩니다. 속성은 hx-*로 시작되며, 다음과 같은 속성을 가지고 있습니다

  1. hx-{httpMethod}: GET 또는 POST등의 http method를 사용할 수 있으며, 요청할 URL을 작성해주면 됩니다.
  2. hx-trigger: URL로 요청할 trigger를 작성합니다. 대표적으로 click, load, submit등이 있습니다.
  3. hx-target: 서버로부터 받은 Page를 넣을 위치를 정의합니다.
  4. hx-swap: 서버로부터 받은 Page를 어떻게 교체할 것인지 정의합니다.

예제1. Load와 함께 Rendering

Pageload가 된 후 서버에 GET 요청하여 HTML를 받아서 Rendering 하는 예제를 Spring BootThymeleaf로 구현해보겠습니다.
아래의 코드는 /load로 요청이 오면, load.html을 응답을 주고, /get-load 요청이 오면 Hello, htmx를 반환해주는 Controller입니다. 여기서 /get-load.html 파일을 통해 응답을 줄 수 있지만, 간단한 예시이므로 @ResponseBody를 통해 바로 HTML을 응답 주도록 구성했습니다.

@Controller
public class LoadController {
    @GetMapping("/load")
    public String index() {
        return "load";
    }

    @GetMapping("/get-load")
    @ResponseBody
    public String load() {
        return "<h1>Hello, htmx</h1>";
    }
}

아래의 코드는 load.html이며, 최초에 브라우저에 보여줄 Page 입니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>LoadTest</title>
</head>
<body>
    Betalabs
    <div hx-get="/get-load" hx-trigger="load delay:2s">
    </div>
<script src="https://unpkg.com/htmx.org@1.8.6"></script>
</body>
</html>

위의 코드를 실행해보면 다음과 같은 결과를 볼 수 있습니다.
Betalabs라는 글자가 보이고, 2초뒤에 자동으로 Hello, htmx를 볼 수 있습니다.
여기서 사용된 태그와 값은 다음과 같습니다.

  1. hx-get: 서버로 /get-loadGET으로 요청합니다.
  2. hx-trigger: hx-get을 실행시킬 조건을 설정합니다.
  3. load: Pageload가 되면 실행이 될 수 있도록 명시했습니다.
  4. delay: 조건이 충족된 후 몇 초 뒤에 실행이 될 건지 명시했습니다. 예제에서는 Page load 후 2초뒤에 요청이 됩니다. 만약 delay를 정의하지 않는다면 즉시 요청됩니다.

해당 예제를 통해서 htmx의 사용법은 매우 간단하고, 강력한것을 알 수 있습니다.

예제2: 쓰기와 폴링

예제1에서는 간단하게 htmx가 무엇인지 알았다면, 이번 예제에서는 데이터를 쓰고 3초에 한 번씩 폴링하는 예제를 작성해보겠습니다. @PostMapping을 통해 POST만 요청을 받아 게시글을 작성할 수 있도록 구현하고, @GetMapping을 통해 작성된 게시글의 HTML을 반환하는 예제의 Contoller입니다.

@Controller
public class BoardController {
    private List<String> boards = new ArrayList<>();

    @GetMapping("/board")
    public String index() {
        return "board";
    }

    @PostMapping("/post-board")
    @ResponseBody
    public String board(String title) {
        boards.add(title);

        return "<p style='color:red'>작성을 완료했습니다.</p>";
    }

    @GetMapping("/get-board")
    @ResponseBody
    public String board() {
        String template = "<div>%s</div>";

        StringBuilder result = new StringBuilder();

        for(String board: boards) {
            result.append(String.format(template, board));
        }

        return result.toString();
    }
}

아래의 코드는 board.html이며, 최초에 브라우저에 보여줄 Page 입니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>BoardTest</title>
</head>
<body>
    <form hx-post="/post-board" hx-trigger="submit">
        제목: <input type="text" name="title">
        <button type="submit">글 쓰기</button>
    </form>

    <div hx-get="/get-board" hx-trigger="every 3s">

    </div>
<script src="https://unpkg.com/htmx.org@1.8.6"></script>
</body>
</html>

예제를 실행하면 초기에는 Data가 담긴 어떤 Page도 보이지 않고, 입력 Page만 보입니다. 제목을 입력하고 글쓰기 버튼을 클릭하면 서버로 글쓰기 요청이 전달되고, 서버는 List에 저장합니다. 그 후 응답으로 빨간 글씨로 저장이 되었다는 것을 알려줍니다. 그리고 form 밑에 정의된 div에서 3초에 한 번씩 /get-board에 요청하여 저장된 결과를 볼 수 있습니다.
우리는 이번 예제를 통해 더 많은 htmx 사용법을 알게 되었습니다.

  1. hx-post: POST 요청을 보낼 수 있습니다.
  2. submit: submit이 되면 post-board에 요청을 할 수 있도록 설정하였습니다.
  3. every: 폴링을 위해 매번 정의된 시간에 한 번씩 요청할지 정하였습니다.

예제3: Infinity Scroll

Javascript 없이 많은 기능을 구현해야 htmx를 사용하는 많은 의미가 있을 것 같습니다. htmx는 사용자 경험 향상을 위해 많은 기능을 구현할 수 있도록 제공하고 있고, 기능을 활용하여 Infinity Scroll도 쉽게 구현이 가능합니다.
인피니티 스크롤은 Facebook이나 Instagram에서 마지막의 피드를 보게 되면 새로운 피드를 얻어오는 것처럼 끝없는 스크롤을 의미합니다. 아래의 코드는 /infinity-scroll 요청이 들어오면 infinity-scroll.html을 응답하고, /get-infinity-scroll 요청으로 들어오면 div 태그를 10개를 생성해서 반환을 주는 Controller 입니다. 여기서 중요하게 봐야 하는 부분은 마지막 태그에는 더 많은 속성을 부여한 것입니다.

@Controller
public class InfinityScrollController {
    @RequestMapping("/infinity-scroll")
    public String index() {
        return "infinity-scroll";
    }

    @GetMapping("/get-infinity-scroll")
    @ResponseBody
    public String infinityScroll(@RequestParam(defaultValue = "1") Integer page) {
        String generalTemplate = "<div>page: %d index: %d</div>";
        String lastTemplate =
                "<div hx-get='/get-infinity-scroll?page=%d'" +
                "     hx-trigger='revealed'" +
                "     hx-swap='afterend'>" +
                "   page: %d index: %d" +
                "</div>";

        StringBuilder result = new StringBuilder();

        for (int index = 0; index < 10; index++) {
            if (index <= 8) {
                result.append(String.format(generalTemplate, page, index));
            } else {
                int nextPage = page + 1;

                result.append(String.format(lastTemplate, nextPage, page, index));
            }
        }

        return result.toString();
    }
}

아래의 코드는 infinity-scroll.html이며, 최초에 브라우저에 보여줄 Page 입니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>InfinityScrollTest</title>
</head>
<body>
    <div id="rows" hx-get="/get-infinity-scroll" hx-trigger="load">
    </div>
<script src="https://unpkg.com/htmx.org@1.8.6"></script>
</body>
</html>

위의 코드를 실행해보면 초기에 load가 되었을 때 /get-infinity-scroll을 요청하여 Rendering을 하고, 마지막 Data가 화면에 보였을 때 다음 Page를 요청하는 것을 확인할 수 있습니다.
여기서 htmx가 제공하는 기능을 활용하여 Infinity Scroll을 구현했으며, 다음과 같은 속성과 값을 사용했습니다.

  1. revealed: 해당 값으로 설정하여 마지막 Data가 보이면 trigging을 할수 있습니다.
  2. afterend: HTML 전체를 수정하는 것이 아니라, 결과를 태그 뒤에 붙이는 것으로 정의했습니다.

불편한점

아직 사용하면서 큰 불편함을 느끼지는 못했지만, WebSocket을 사용할 때는 고민이 필요해 보입니다. htmx에서 제공하는 WebSocketSockJsSTOMP를 제공하지 않는 것으로 보이고, WebSocket 또한 HTML을 통해 응답받아야 합니다.

마치며

누가 쓰면 좋을까?

백엔드 팀이 혼자 작업하는 백오피스나 프론트 팀을 따로 구성하기 어려운 환경에 있는 스타트업 또는 토이 프로젝트의 경우 htmx는 도입해도 괜찮을 것 같습니다.

결론

htmx를 사용하면 백엔드 팀만으로 SPA의 특징을 구현할 수 있으며, 쉽게 사용자 경험 향상을 할 수 있습니다. 해당 글은 간단한 예제를 통해 htmx를 소개했지만, htmx는 더욱 많은 기능을 소개하고 있으니 문서를 통해 공부를 해보는 것을 추천하며 Spring에서 Thymeleaf를 확장한 기능도 오픈소스로 제공하고 있으니, 참고하시면 더 쉽고 유연하게 개발할 수 있습니다.

https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf

1개의 댓글

comment-user-thumbnail
2023년 4월 12일

제가 이해한 바로는 "나 혼자 다 해먹을 htmx"는 혼자서 HTMX라는 기술을 모두 습득하겠다는 의지를 나타내는 말인 것 같습니다.

HTMX는 HTML, CSS, JavaScript를 사용하여 비동기적으로 서버와 클라이언트 간의 상호작용을 구현하는 웹 프론트엔드 기술입니다. HTMX는 AJAX, WebSockets 및 기타 비동기 기술과 같은 전통적인 웹 개발 패턴과는 다르게, 기존 HTML 요소와 이벤트를 사용하여 서버와의 상호작용을 처리합니다.

HTMX를 혼자서 모두 습득하고 응용할 수 있다면, 높은 수준의 웹 개발 기술을 습득할 수 있을 것입니다. HTMX를 공부하기 위해서는 HTML, CSS, JavaScript, 웹 개발의 기본적인 개념에 대한 이해가 필요합니다. 또한 HTMX는 서버와의 상호작용을 처리하기 때문에 백엔드 개발에 대한 이해도 필요합니다.
MyHealthAtVanderbilt Login
HTMX를 공부하는 것은 웹 개발 분야에서 경쟁력 있는 역량을 갖추는 것에 도움이 될 수 있습니다. 하지만 혼자서 모든 것을 습득하는 것은 어렵기 때문에 관심 있는 분야에서 팀원들과 함께 공부하고 협업하는 것이 좋습니다.

답글 달기