프로필

데브고래밥

@devgoraebap

스택오버플로우의 단골손님이였던 Claude를 채찍질하는 개발자

Album Art

0:00 0:00
방문자 정보

요즘 관심있는

HDA 시리즈 #2 - 삭제 기능과 에러 처리 thumbnail image
47
0

HDA 시리즈 #2 - 삭제 기능과 에러 처리

이번 글에서는 Todo 삭제 기능을 구현하면서, HTMX에서 상황에 따라 응답을 유연하게 처리하는 방법을 알아본다.

예제 코드: https://github.com/dev-goraebap/hypermedia-driven-demo/tree/step-02

삭제 버튼 추가

각 Todo 아이템에 삭제 버튼을 추가한다.

@param com.example.demo.app.domain.Todo todo = null

@if(todo != null)
    <li class="p-3 border-b flex justify-between">
        <p>${todo.getContent()}</p>
        <button hx-delete="/todos/${todo.getId()}"
                hx-trigger="click"
                hx-target="closest li"
                hx-swap="outerHTML"
                hx-target-4xx="#HTMX_TODO_DELETE_ERROR"
                class="text-xs text-red-500">
            삭제
        </button>
    </li>
@endif

closest 선택자

hx-target="closest li"는 버튼에서 가장 가까운 <li> 요소를 타겟으로 지정한다.

만약 closest를 사용하지 않는다면, 각 아이템마다 고유한 ID를 부여해야 한다.

<!-- closest 없이 구현하면 -->
<li id="todo-${todo.getId()}">
    <button hx-target="#todo-${todo.getId()}">삭제</button>
</li>

closest를 사용하면 이런 번거로운 ID 관리가 필요 없다. 참고로 closest는 HTMX만의 기능이 아니라 브라우저에서 기본으로 제공하는 표준 DOM API다. (MDN 문서)

에러 처리의 문제점

삭제 기능을 처음 구현했을 때 문제가 있었다. 삭제 요청이 실패했는데 아이템이 사라져버리는 현상이었다.

원인은 이랬다. hx-target="closest li"hx-swap="outerHTML"을 설정하면, 서버 응답이 해당 <li>를 교체한다. 성공 시에는 빈 응답으로 아이템이 삭제되어야 하지만, 실패 시에도 에러 응답이 <li>를 교체해버려서 아이템이 사라지는 것이다.

HTMX 기본 기능만으로는 "성공하면 A 타겟, 실패하면 B 타겟"처럼 응답 상태에 따라 다른 처리를 하기 어렵다.

Response Targets Extension

이 문제를 해결하기 위해 response-targets 익스텐션을 사용한다. HTTP 상태 코드에 따라 다른 타겟을 지정할 수 있다.

새로운 라이브러리를 추가하는 것이 부담스러울 수 있지만, response-targets는 HTMX 개발팀이 직접 관리하는 공식 core extension이다. HTMX와 함께 안정적으로 유지보수되므로 편하게 사용해도 될것같다. (공식 문서)

<!-- htmx 다음에 extension 추가 -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4"></script>

extension을 사용하려면 부모 요소에 hx-ext="response-targets"를 선언해야 한다. 자식 요소들에게 상속되므로 최상위에 한 번만 선언하면 된다.

<div hx-ext="response-targets">
    <!-- 이 안의 모든 요소에서 response-targets 사용 가능 -->
</div>

hx-target-4xx 속성

이제 hx-target-4xx 속성으로 4xx 에러 시 다른 타겟을 지정할 수 있다.

<button hx-delete="/todos/${todo.getId()}"
        hx-target="closest li"
        hx-swap="outerHTML"
        hx-target-4xx="#HTMX_TODO_DELETE_ERROR">
    삭제
</button>
  • 성공 (2xx): closest liouterHTML로 교체
  • 실패 (4xx): #HTMX_TODO_DELETE_ERROR에 에러 메시지 표시

상태 코드별로 다양한 속성을 사용할 수 있다.

  • hx-target-error: 모든 에러 (4xx + 5xx)
  • hx-target-4xx: 400번대만
  • hx-target-5xx: 500번대만
  • hx-target-404: 특정 상태 코드만

컨트롤러 구현

서버에서 에러 시 적절한 HTTP 상태 코드를 반환해야 한다.

@DeleteMapping("{id}")
public String destroy(
        @PathVariable String id,
        Model model,
        HttpServletResponse response
) {
    try {
        todoService.destroy(id);
    } catch (ResponseStatusException ex) {
        response.setStatus(ex.getStatusCode().value());
        model.addAttribute("errors", List.of(ex.getReason()));
        return "pages/todos/_destroyFail";
    }

    return null; // 빈 응답
}

HttpServletResponse로 상태 코드를 설정하고, 에러 시 에러 템플릿을 반환한다. 성공 시에는 return null로 빈 응답을 보내면 hx-swap="outerHTML"에 의해 해당 <li>가 삭제된다.

에러 모달 템플릿

@param java.util.List<String> errors = null

<div id="HTMX_TODO_DELETE_ERROR" hx-swap-oob="true">
    <div>
        @if(errors!=null && !errors.isEmpty())
            <div class="fixed inset-0 bg-black/70" onclick="this.parentNode.remove()"></div>
            <div class="fixed left-[50%] translate-x-[-50%] top-[50%] translate-y-[-50%]
                border-l-4 border-red-500 text-red-700 rounded-xl p-3 bg-white">
                @for(var error: errors)
                    <p>${error}</p>
                @endfor
            </div>
        @endif
    </div>
</div>

삭제 실패 시 모달 형태로 에러 메시지를 표시한다. 배경을 클릭하면 모달이 닫힌다.

삭제 실패 시 다음과 같이 에러 모달이 표시된다.

현재 방식의 한계

지금까지 구현한 방식은 생성 시 새 아이템만, 삭제 시 해당 아이템만 업데이트하는 부분 업데이트 방식이다. 전체 HTML을 교체하지 않으므로 네트워크 비용이 줄어드는 장점이 있다.

하지만 이 방식은 Todo 리스트처럼 단순한 구조에서 가능한 것이다. 실무에서 다루는 복잡한 폼의 경우, 요청 결과에 따라 화면 여러 곳이 변경되어야 하는 경우가 많다. 이런 상황에서는 부분만 직접 업데이트하는 방식이 오히려 복잡해질 수 있다.

다음 글에서는 서버측에서 HX-Location 헤더를 활용해 성공 시 페이지를 새로고침하는 방법을 다룰 예정이다. HTMX가 이 헤더를 감지하면 전체 페이지 리로드 없이 AJAX로 해당 URL의 콘텐츠를 가져와 교체한다.

정리

  • closest 선택자: 가장 가까운 부모 요소를 타겟으로 지정, ID 관리 불필요
  • response-targets extension: HTTP 상태 코드별로 다른 타겟 지정 가능
  • hx-target-4xx: 4xx 에러 시 에러 메시지 표시 영역으로 타겟 변경
  • return null: 빈 응답으로 요소 삭제 처리

다음 글에서는 HX-Location 헤더를 활용한 서버 주도 리다이렉트를 다룰 예정이다.