목록을 다시 그렸더니 버튼이 동작하지 않은 이유
문제 발생
목록 항목마다 삭제 버튼에 리스너를 달았습니다.
document.querySelectorAll(".delete").forEach((btn) =>
btn.addEventListener("click", onDelete),
);검색으로 목록을 다시 그린 뒤부터 버튼이 아무 반응도 하지 않았습니다. 새로 만들어진 버튼에는 리스너가 없었기 때문입니다. 매번 다시 등록하도록 고쳤더니 이번에는 리스너가 중복으로 쌓였습니다.
원인 분석
리스너는 요소에 붙습니다. querySelectorAll은 그 시점의 요소들을 반환할 뿐이고, 이후 만들어진 요소와는 아무 관계가 없습니다.
브라우저의 이벤트 전파를 쓰면 이 문제가 사라집니다. MDN은 버블링을 이렇게 설명합니다 — 부모 요소에 리스너를 달고 사용자가 자식을 클릭하면 이벤트가 클릭된 가장 안쪽 요소에서 위로 버블링되어 부모에서도 발생합니다. 단계는 캡처 → 타깃 → 버블이고, 기본적으로 거의 모든 핸들러가 버블 단계에 등록됩니다.
판별에 쓰는 두 값의 차이도 문서에 있습니다 — event.target은 이벤트가 처음 발생한 요소이며 버블링 중에도 그대로 유지되고, event.currentTarget은 현재 핸들러가 붙어 있는 요소라 계층마다 달라집니다.
이 성질을 이용한 것이 이벤트 위임입니다 — 문서 설명대로 여러 자식에 리스너를 각각 붙이는 대신 부모에 하나만 붙이고 event.target으로 어떤 자식이 클릭됐는지 판별합니다.
해결 방안
- 부모 하나에 걸고 위임합니다. 목록을 다시 그려도 리스너는 그대로입니다.
list.addEventListener("click", (event) => {
const button = event.target.closest(".delete");
if (!button || !list.contains(button)) return;
onDelete(button.dataset.id);
});-
closest()를 함께 씁니다. 버튼 안에 아이콘이 있으면event.target은 그 아이콘입니다 — 정확히 비교하는 대신 조상 방향으로 찾습니다. -
필요한 정보는
data-*에 담습니다. 항목 id를 DOM에 두면 위임 핸들러가 별도 조회 없이 처리할 수 있습니다. -
stopPropagation()은 신중하게 씁니다. 문서 설명대로 이벤트가 부모로 올라가는 것을 막는데, 그 부모가 위임 핸들러라면 다른 기능이 조용히 멈춥니다. -
버블링하지 않는 이벤트를 확인합니다.
focus,blur같은 이벤트는 버블링하지 않아 위임이 그대로 동작하지 않습니다 —focusin/focusout을 씁니다. -
프레임워크를 쓴다면 대개 필요 없습니다. React·Vue의 이벤트 바인딩은 리렌더 때 자동으로 유지되므로, 이 패턴은 주로 바닐라 DOM이나 서버 렌더 HTML에 동작을 붙일 때 씁니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.