JavaScript 이벤트 위임으로 동적 목록 처리하기 — 직접 바인딩과 비교 검증

조회수 4

도입

페이지가 처음 열릴 때 모든 버튼에 클릭 리스너를 연결했는데, 이후 추가한 버튼만 반응하지 않는 경우가 있다. 원인은 새 요소가 초기 querySelectorAll() 결과에 포함되지 않았기 때문이다.

이 글은 순수 JavaScript로 실패를 재현하고, 클릭 이벤트의 버블링과 closest()를 이용해 목록 부모의 리스너 하나로 초기 요소와 동적 요소를 함께 처리한다.

직접 바인딩이 실패하는 이유

querySelectorAll()은 호출 시점에 일치한 요소 목록을 반환한다. 그 목록에 리스너를 연결해도 나중에 생성한 노드에 같은 리스너가 자동 복제되지는 않는다.

document.querySelectorAll('.delete-button').forEach((button) => {
  button.addEventListener('click', () => console.log(button.dataset.id));
});

document.querySelector('#add-button').addEventListener('click', () => {
  const item = document.createElement('li');
  item.innerHTML = '<button class="delete-button" data-id="new">삭제</button>';
  document.querySelector('#item-list').append(item);
});

새 항목을 만들 때마다 리스너를 다시 연결할 수는 있다. 하지만 생성 경로가 여러 개면 연결 누락과 중복 등록을 추적하기 어려워진다.

초기 버튼과 달리 동적으로 추가된 버튼의 클릭 로그가 없는 화면
초기 조회 결과는 정상적으로 나온다
초기 버튼과 달리 동적으로 추가된 버튼의 클릭 로그가 없는 화면
나중에 추가된 버튼은 처리되지 않는다.

이벤트 위임의 동작 원리

클릭 이벤트는 실제로 클릭된 요소에서 시작해 조상 요소 방향으로 전달된다. 이를 이벤트 버블링이라고 한다. 목록 부모가 버블링된 클릭을 받으면 어떤 자식에서 시작됐는지 event.target으로 확인할 수 있다.

flowchart LR A[내부 아이콘 클릭] --> B[삭제 버튼] B --> C[목록 항목] C --> D[목록 부모 리스너]

event.currentTarget은 현재 핸들러가 연결된 목록 부모다. 두 값을 구분해야 클릭된 자식과 이벤트를 처리하는 경계를 혼동하지 않는다.

안정적인 부모에 리스너 연결하기

closest로 실제 버튼 찾기

const list = document.querySelector('#item-list');

list.addEventListener('click', (event) => {
  const button = event.target.closest('.delete-button');
  if (!button || !list.contains(button)) return;

  console.log({
    target: event.target.tagName,
    currentTarget: event.currentTarget.id,
    id: button.dataset.id
  });
});

버튼 안의 아이콘이나 span을 클릭하면 event.target은 버튼이 아닐 수 있다. closest()는 자신부터 조상 방향으로 선택자와 일치하는 첫 요소를 찾는다. 이어서 list.contains(button)을 검사하면 목록 밖의 요소를 처리하는 실수를 막을 수 있다.

이벤트 위임으로 초기 버튼과 동적 버튼의 식별자가 모두 기록된 콘솔
부모에 연결한 리스너는 버블링된 두 클릭을 같은 코드로 처리한다.

적합한 범위

  • 행이 추가·삭제되는 목록과 테이블
  • 같은 동작을 수행하는 자식 요소가 많은 영역
  • 서버 응답으로 내부 DOM을 교체하는 화면

이벤트가 버블링되지 않거나 자식마다 다른 정리 로직이 필요하면 개별 리스너가 더 명확할 수 있다. 위임 범위를 document 전체로 넓히기보다 기능을 소유한 가장 가까운 안정적인 부모를 선택한다.

이벤트 경계에서 확인할 두 가지

버튼 안의 아이콘을 클릭하면 동작하지 않는다

증상

버튼 글자를 누르면 되지만 내부 SVG를 누르면 조건을 통과하지 못한다.

원인

event.target.matches('.delete-button')만 검사해 실제 target인 SVG를 놓친다.

확인 방법

console.log(event.target, event.target.closest('.delete-button'));

해결

closest()로 버튼을 찾고 위임 부모 내부인지 검사한다.

예방

중첩 마크업이 있는 컨트롤은 실제 클릭 target이 바뀔 수 있다는 전제로 테스트한다.

다른 기능의 클릭까지 처리된다

증상

페이지의 무관한 버튼이 같은 선택자를 사용해 잘못 처리된다.

원인

리스너를 너무 높은 조상에 연결했거나 소유 영역 검사가 없다.

확인 방법

Console에서 currentTarget과 찾은 버튼의 부모 경로를 확인한다.

해결

리스너를 기능 컨테이너로 내리고 contains() 검사를 유지한다.

예방

선택자뿐 아니라 컴포넌트 경계를 이벤트 처리 조건으로 사용한다.

적용 체크리스트

  • 리스너를 기능 영역의 가장 가까운 안정적 부모에 연결했는가?
  • closest() 결과가 위임 영역 안에 있는지 확인하는가?
  • 초기 요소와 동적으로 추가한 요소를 모두 클릭해 확인했는가?
  • 키보드 조작이 필요한 UI라면 접근성 동작도 확인했는가?

참고 자료

한 줄 요약

이벤트 위임은 동적 자식의 버블링 이벤트를 가까운 안정적 부모에서 받고 closest()와 영역 검사로 정확한 대상을 찾는 방식이다.

댓글 0

  • 첫 번째 댓글을 남겨보세요.