본문 바로가기

JavaScript/모던 자바스크립트 튜토리얼

모던 자바스크립트 튜토리얼 | 이벤트 위임

캡처링과 버블링을 활용하면 강력한 이벤트 핸들링 패턴인 이벤트 위임(event delegation)을 구현할 수 있습니다.

 

이벤트 위임은 비슷한 방식으로 여러 요소를 다뤄야할 때 사용됩니다. 이벤트 위임을 사용하면 요소마다 핸들러를 할당하지 않고, 요소의 공통 조상에 이벤트 핸들러를 단 하나만 할당해도 여러 요소를 한꺼번에 다룰 수 있습니다.

 

공통 조상에 할당한 핸들러에서 event.target을 이용하면 실제 어디서 이벤트가 발생했는지 알 수 있습니다. 이를 이용해 이벤트를 핸들링합니다.

 

예제를 살펴봅시다. 다음은 고대 중국 철학에서 유래한 팔괘도입니다. 

 

HTML은 대략 다음과 같습니다.

 

<table>
  <tr>
    <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="n">...</td>
    <td class="ne">...</td>
  </tr>
  <tr>...2 more lines of this kind...</tr>
  <tr>...2 more lines of this kind...</tr>
</table>

지금 해야 할 작업은 <td>를 클릭했을 때, 그 칸을 강조하는 것입니다. 각 <td> 마다 onclick 핸들러를 할당하는 대신, '모든 이벤트를 잡아내는' 핸들러를 <table> 요소에 할당해 보겠습니다. table 요소에 할당하게 될 핸들러는 event.target을 이용해 어떤 요소가 클릭되었는지 감지하고, 해당 칸을 강조하게 됩니다. 

 

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // 클릭이 어디서 발생했을까요?

  if (target.tagName != 'TD') return; // TD에서 발생한 게 아니라면 아무 작업도 하지 않습니다,

  highlight(target); // 강조 함
};

function highlight(td) {
  if (selectedTd) { // 이미 강조되어있는 칸이 있다면 원상태로 바꿔줌
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // 새로운 td를 강조 함
}

 

이렇게 코드를 작성하면 테이블 내 칸의 개수는 고민거리가 되지 않습니다. 강조 기능을 유지하면서 <td>를 언제라도 넣고 뺄 수 있게 됩니다. 하지만 단점도 있습니다. 위와 같이 구현하면 클릭 이벤트가 <td>가 아닌 <td> 안에서 동작할 수 있습니다. 

 

팔괘도의 HTML을 살펴봅시다. <td> 안에 중첩 태그 <strong>이 있는 것을 확인할 수 있습니다.

 

<td>
  <strong>Northwest</strong>
  ...
</td>

<strong>을 클릭하면 event.target에 <strong>에 해당하는 요소가 저장됩니다.

 

따라서 table.onclick 핸들러에서 event.target을 이용해 클릭 이벤트가 <td> 안쪽에서 일어났는지 아닌지를 알아내야 합니다. 이런 단점을 반영하여 기능을 향상한 코드는 아래와 같습니다.

 

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

1: elem.closest(selector) 메서드는 elem의 상위 요소 중 selector와 일치하는 가장 근접한 조상 요소를 반환합니다. 위 코드에선 이벤트가 발생한 요소부터 시작해 위로 올라가며 가장 가까운 <td> 요소를 찾습니다.

2. event.target이 <td> 안에 있지 않으면 그 즉시 null을 반환하므로 아무 작업도 일어나지 않습니다. 

3. 중첩 테이블이 있는 경우 event.target은 현재 테이블 바깥에 있는 <td>가 될 수 있습니다. 이런 경우를 처리하기 위해 <td>가 팔괘도 안에 있는지를 확인합니다. 

4. 이제 진짜 td를 강조합니다.

 

이렇게 구현하면 <td>의 개수에 상관없이 원하는 <td>를 강조해주는 코드를 빠르고 효율적으로 구현할 수 있습니다.

 

이벤트 위임 활용하기

이벤트 위임을 다른 식으로도 활용할 수 있습니다. '저장하기', '불러오기', '검색하기'등의 버튼이 있는 메뉴를 구현해야 한다고 가정해봅시다. 각 버튼의 기능과 관련된 메서드 save, load, search가 있는 객체도 이미 구현한 상태입니다. 이럴 때 버튼과 메서드를 어떻게 연결할 수 있을까요?

 

가장 먼저 버튼 각각에 독립된 핸들러를 할당하는 방법이 떠오를 것입니다. 하지만 이 방법보다 더 우아한 해결책이 있습니다. 메뉴 전체에 핸들러를 하나 추가해주고, 각 버튼의 data-action 속성에 호출할 메서드를 할당해주는 방법 말이죠.

<div id="menu">
  <button data-action="save">저장하기</button>
  <button data-action="load">불러오기</button>
  <button data-action="search">검색하기</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('저장하기');
    }

    load() {
      alert('불러오기');
    }

    search() {
      alert('검색하기');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action]();
      }
    };
  }

  new Menu(menu);
</script>

 

*로 표시한 줄의 this.onClick은 this에 바인딩했다는 점에 주의해주세요. 이렇게 하지 않으면 this는 Menu 객체가 아닌 DOM 요소(elem)을 참조하게 됩니다. 이렇게 되면 this[action]에서 원하는 것을 얻지 못합니다. 

 

이런 식으로 이벤트 위임을 활용하면 어떤 이점이 있을까요? 아래와 같은 장점이 있습니다. 

 

- 버튼 마다 핸들러를 할당해주는 코드를 작성할 필요가 없어집니다. 메서드를 만들고 HTML에 그 메서드만 써주기만 하면 됩니다. 

- 언제든지 버튼을 제거하고 추가할 수 있어 HTML 구조가 유연해집니다.

 

.action-save, .action-load 같은 클래스를 사용할 수도 있지만, data-action 속성이 좀 더 의미론적으로 낫습니다. CSS 규칙을 적용할 수도 있게 됩니다.

 

행동 패턴

이벤트 위임은 요소에 선언적 방식으로 '행동(behavior)'을 추가할 때 사용할 수도 있습니다. 이때는 특별한 속성과 클래스를 사용합니다. 행동 패턴은 두 부분으로 구성됩니다. 

 

1. 요소의 행동을 설명하는 커스텀 속성을 요소에 추가합니다.

2. 문서 전체를 감지하는 핸들러가 이벤트를 추적하게 됩니다. 1에서 추가한 속성이 있는 요소에서 이벤트가 발상하면 작업을 수행합니다.

 

첫 번째 카운터: <input type="button" value="1" data-counter>
두 번째 카운터: <input type="button" value="2" data-counter>

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // 속성이 존재할 경우
      event.target.value++;
    }

  });
</script>

 

버튼을 클릭하면 숫자가 증가합니다. 이 예시에서 집중해야 할 것은 버튼이 아니고 접근방식입니다. data-counter 속성이 있는 요소는 원하는 만큼 만들 수 있습니다. 필요할 때마다 HTML에 추가해주면 되니까요 예시에선 이벤트 위임을 사용해 새로운 행동을 선언해주는 속성을 추가해서 HTML을 '확장'하였습니다.

 

document 레벨의 핸들러를 만들 땐 항상 addEventListener를 사용하세요. document 객체에 핸들러를 할당할 때는 docment.onclick을 사용해선 안됩니다. document.onclick은 충돌을 일으킬 가능성이 있기 때문에 무조건 addEventListener를 사용해야 합니다. document.onclick은 새로운 핸들러가 이전의 핸들러를 덮어쓸 수 있습니다. 코드 곳곳에서 document에 다수의 핸들러를 할당할 수 있습니다. 실제 프로젝트에서 이는 자연스러운 일입니다. 

 

행동 패턴에 관한 예시를 하나 더 살펴봅시다. 이번엔 data-toggle-id 속성이 있는 요소를 클릭하면 속성값이 id인 요소가 나타나거나 사라지게 해보겠습니다.

 

<button data-toggle-id="subscribe-mail">
  구독 폼 보여주기
</button>

<form id="subscribe-mail" hidden>
  메일 주소: <input type="email">
</form>

<script>
  document.addEventListener('click', function(event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>

자바스크립트를 사용하지 않고도 요소에 토글 기능을 추가할 수 있다는 점에 주목합시다. 태그에 data-toggle-id 속성만 추가하면 요소를 토글할 수 있습니다.

 

행동 패턴을 응용하면 토글 기능이 필요한 요소 전체에 자바스크립트로  해당 기능을 구현해 주지 않아도 되기 때문에 매우 편리합니다. 행동을 선언해주기만 하면 되기 때문입니다. 문서 레벨에 적절한 핸들러를 구현해주기만 하면 페이지 내 모든 요소에 행동을 쉽게 적용할 수 있습니다. 한 요소에 여러개의 행동을 조합해 적용하는 것도 가능합니다. 

 

 

 

이벤트 위임

 

ko.javascript.info