[DOM] 이벤트 위임(Event Delegation)

김효식 (HS KIM)·2021년 12월 28일
0
post-thumbnail

들어가며

최근에 바닐라 자바스크립트로 기능을 구현하던 도중에, 하위 요소들에 같은 이벤트 리스너를 바인딩해야 되는 일이 있었다. 이전에 리액트를 사용했을 때는 이 부분에 대한 큰 고민없이 이벤트를 발생시키는 요소에 직접 이벤트 리스너를 바인딩 하는 것이 당연하다고 생각했다. 그런데 하위 요소마다 같은 이벤트 리스너를 바이딩했을 때, 하위 요소의 수가 적다면 그 차이는 미비하겠지만 하위 요소가 많다면 이벤트 리스너가 하위 요소의 수만큼 중복되기 때문에 메모리 효율적인 측면에서 바람직하지 않다고 생각했다.

이벤트 위임이란?

위와 같은 상황에서는 하위 요소들을 묶어줄 수 있는 상위요소에 이벤트 리스너를 한번만 바인딩해주고, 하위 요소들에게 이벤트가 발생했을 때 상위 요소에 바인딩된 이벤트 리스너를 호출하는 것이 효율적일 것이다. 그리고 이것을 상위 요소의 이벤트가 하위 요소에 전달된다고 해서 이벤트 위임(Event Delegation)이라고 한다.

<body>
  <section class="button-container">
    <button class="red-btn">
      <img src="./red-button.png" alt="red-btn" />
      <span>1</span>
    </button>
    <button class="red-btn">
      <img src="./red-button.png" alt="red-btn" />
      <span>2</span>
    </button>
    <button class="red-btn">
      <img src="./red-button.png" alt="red-btn" />
      <span>3</span>
    </button>
  </section>
</body>

이벤트 위임에 대해 알아보기 위해 간단한 코드를 작성했다.

이벤트 위임을 사용하지 않았을 때의 문제점

const handleEvent = e => {
  console.log(e.target);
};

document.querySelectorAll('.red-btn').forEach($btn => $btn.addEventListener('click', handleEvent));

querySelectorAll로 모든 button을 가져와 각각의 버튼에 이벤트 리스너를 바인딩해줬다. 기능은 정상적으로 동작하지만 같은 함수를 반복하여 이벤트 리스너로 바인딩하기 때문에 메모리 사용 측면에서 비효율적이다.

그리고 만약 하위 요소가 추가된다면 하위 요소를 추가할 때마다 새롭게 추가되는 요소에 이벤트 리스너를 바인딩해야 한다. 추가되지 않고 초기에는 하위 요소가 존재하지 않다가 동적으로 추가 되는 경우도 마찬가지다. 그렇기 때문에 하위 요소에 일일이 추가하는 것보다 상위 요소에 이벤트 리스너를 바인딩하여 이벤트 위임을 해주는 것이 바람직해 보인다.

그런데 사실 지금 button도 이벤트 위임을 하고 있다.

사진에 표시한 위치와 순서대로 버튼을 클릭하면 콘솔에는 이렇게 나온다.

button의 하위요소인 <img><span>에는 이벤트 리스너를 직접 바인딩하지 않았지만 상위 요소인 button의 이벤트 리스너가 호출된 것이다.

css 속성을 통한 문제 해결

이제 button의 내부를 클릭하면 어느 곳을 클릭하더라도 button에 바인딩한 이벤트 리스너가 호출된다.

.red-btn > img,
.red-btn > span {
   pointer-events: none;
 }

그런데 만약 함수 호출 뿐만 아니라 target도 button으로 가져오고 싶다면 target을 지정하고 싶지 않은 하위 요소에 csspointer-events: none속성을 적용하는 것이다. 해당 속성을 적용하면 <img><span>요소를 클릭해도 button이 이벤트의 target이 된다.

이제 button의 하위요소를 클릭해도 이벤트 리스너가 동작한다. 근데 button에 이벤트 위임을 하기 위해 button-container에 이벤트 리스너를 바인딩한다면 button의 외부 영역에도 이벤트 리스너가 바인딩 되어 있다. 그래서 1과 2를 순차적으로 클릭하여 콘솔에 나온 요소는 같지 않다.

그래서 .button-container에도 pointer-events: none 속성을 주려고 하면, 이제 콘솔에 아무런 값도 나오지 않는다. pointer-events: none속성은 하위 요소에도 적용되기 때문에 .button-container와 모든 하위요소들이 이벤트의 target 대상이 되지 않는다.

그래서 이 문제를 해결하기 위해 위에서 작성한 handleEvent함수에 이벤트의 target이 해당 button인지 판별해주는 로직을 추가해줄 필요가 있었다.

const handleEvent = e => {
  if (!e.target.classList.contains('red-btn')) return;
  console.log(e.target);
};

button의 하위 요소에는 pointer-events: none 속성을 적용하고, button이 아닌 이벤트 리스너를 바인딩 해준 상위 요소의 영역은 자바스크립트로 판별하여 이벤트 위임을 구현하였다.

css 속성의 문제점과 자바스크립트를 통한 문제 해결

그런데 프론트엔드는 항상 크로스 브라우징의 문제를 안고 있다. pointer-events속성은 현재 대부분의 브라우저에서 지원하지만, 일부 구형 브라우저에서는 지원하지 않는다.

이외에도 발생할 수 있는 문제가 더 존재하는데, html의 구조가 지금보다 더 복잡해질 수 있다. button의 하위 요소의 하위 요소가 존재하고 그 하위 요소는 별도의 event를 지정해줘야 한다면 (그런 마크업 구조가 유용한지는 현재는 차치하고) pointer-events속성은 하위 요소들에게 상속되기 때문에 별도의 event를 지정할 수 없다.

이런 경우를 제외하고는 css를 사용하는 것이 간단하고 좋지만, 만약 위와 같은 상황에는 자바스크립트를 통해서 문제를 해결할 수 있다.

const handleEvent = ({ target }) => {
  let $element = target;
  while (!$element.classList.contains('red-btn')) {
    $element = $element.parentElement;

    if ($element.matches('body')) {
      return;
    }
  }
  console.log($element);
};

위에서 구현한 handleEvent 함수에서는 .red-btn 버튼 요소를 찾을때까지 계속해서 $element에 부모 요소를 재할당해준다. 만약, 해당 요소 외부 영역을 클릭하면 해당 요소의 부모로 해당 요소를 찾을 수 없으므로 재할당한 값이 body가 되면 return 해주는 것으로 위 문제를 해결했다. 자바스크립트가 반드시 필요한 경우가 아니라면 css를 통해 구현하는 것이 좋다.

const handleEvent = ({ target }) => {
  if(target.matches('.button-container')) return;
  if(target.matches('.red-btn *')) return;
  console.log(target);
};

button의 하위 요소가 복잡하지 않다면 자바스크립트를 사용하지 않고 css 선택자로 button의 부모요소와 하위요소에 이벤트를 return시켜 함수를 호출시키지 않을 수 있다.

profile
자기개발 :)

0개의 댓글