❓이벤트 위임이란
개별 요소 각각에 이벤트 리스너를 생성하는 대신에, 개별 요소의 상위 요소에 이벤트 리스너를 등록한 뒤 조건문을 통해 하위 요소 중 어떤 요소에 이벤트가 발생했는지 특정짓는 것이다.
어떤 상황에서 유용한지 간단하게 알아보자. 아래와 같은 메뉴가 있다고 할 때, 메뉴 리스트에 등록된 각 메뉴 아메리카노, 프라푸치노에 클릭 이벤트를 달아주려고 한다.
위와 같은 메뉴가 있다고 할 때, 메뉴 리스트에 등록된 각 메뉴 아메리카노, 프라푸치노에 클릭 이벤트를 바인딩 하려고 한다. 각각의 자식 노드에 이벤트를 바인딩하려면 아래와 같이 반복문을 돌리거나 혹은 DOM 요소를 찾아 등록해주어야한다.
// #espresso-menu-list 라는 상위 엘리먼트의 각 자식 노드에 이벤트 리스너를 등록한다.
[...$('#espresso-menu-list').children].forEach($menuItem => {
$menuItem.addEventListener('click', e => {
const $menuName = e.target.closest('li').querySelector('.menu-name');
const updatedMenuName = prompt(
'메뉴 이름을 수정하세요',
$menuName.innerText,
);
$menuName.innerText = updatedMenuName;
});
});
현재는 메뉴 item이 두 개 뿐이라 괜찮지만, 메뉴가 100개, 1000개 늘어나면 그에 맞게 모든 요소에 이벤트를 바인딩 해주어야 하므로 성능 저하 뿐만 아니라 유지보수도 어려운 코드가 될 것이다. 그리고 메뉴 item DOM 을 동적으로 생성하는 경우에는 이벤트 바인딩이 정상적으로 되지 않는 문제점이 있다.
이를 이벤트 위임, 즉 부모 요소인 #espresso-menu-list 요소에 이벤트를 위임하여 해결할 수 있다. 코드는 아래와 같다.
// 상위 요소에 이벤트 위임
$('#espresso-menu-list').addEventListener('click', e => {
// 수정 버튼 클릭
if (e.target.classList.contains('menu-edit-button')) {
// 주어진 클래스 이름과 일치하는 가장 가까운 요소 탐색
const $menuName = e.target.closest('li').querySelector('.menu-name');
const updatedMenuName = prompt(
'메뉴 이름을 수정하세요',
$menuName.innerText,
);
$menuName.innerText = updatedMenuName;
}
});
if문으로 현재 클릭한 target의 클래스 이름을 확인하여 클릭 이벤트에 대한 동작을 수행한다. 이렇게 하면 동적으로 생성된 자식 요소에 대해서도 이벤트를 실행할 수 있고, 모든 하위 요소에 직접 이벤트를 바인딩하지 않아도 된다.
어떻게 이게 가능한걸까?
📝이벤트 전파 : 캡처링, 버블링
이렇게 동작할 수 있는 이유를 알기 위해 이벤트 전파 개념을 알아야한다. 이벤트 전파란, 여러개의 중첩된 같은 이벤트 타입의 이벤트 핸들러가 있을 때 어떤 것을 먼저 실행시키는가에 대한 규약이라고 볼 수 있다.
전파 단계에는 두 가지가 있는데, 자식 요소에서 발생한 이벤트가 부모 요소를 거쳐 자식 요소까지 도달하는 부모 -> 자식 방향이 캡처링, 반대로 자식 요소에서 발생한 이벤트가 부모 요소로 전파되는 자식 -> 부모 방향을 버블링이라고 한다.
function handler(event){
var phases = ['capturing', 'target', 'bubbling']
console.log(event.target.nodeName, this.nodeName, phases[event.eventPhase-1]);
}
// target -> filedset -> body -> html 순으로 중첩되어있다.
document.getElementById('target').addEventListener('click', handler, true);
document.querySelector('fieldset').addEventListener('click', handler, true);
document.querySelector('body').addEventListener('click', handler, true);
document.querySelector('html').addEventListener('click', handler, true);
// 코드 출처 : 생활코딩(https://opentutorials.org/module/904/6768)
이렇게 중첩된 html 요소에 각각 같은 타입의 이벤트 리스너를 바인딩한 경우, 세 번째 인자에 따라 두 가지 전파 방향을 갖게 된다. 위 코드와 같이 true 값을 전달한 경우, 캡처링 단계의 이벤트 객체를 캐치할 수 있다. 기본 값은 false이기 때문에 기본적으로는 버블링 단계에서 전파되는 이벤트를 캐치한다.
mdn에 따르면, 이벤트의 전파 단계가 capturing, bubbling 2 단계로 나뉜 것이므로, 어떤 이벤트가 발생하면 캡처링 부터 시작하여 버블링 단계를 거쳐 종료된다.
앞서 기술했듯 기본적으로 모든 이벤트 핸들러가 버블링 단계에 대해 등록되어 있다. 그래서 먼저 중첩된 요소의 가장 안쪽, 자식 요소에 있는 이벤트 핸들러를 실행하고 그 뒤 조상 요소로 이동하여 이벤트 핸들러를 실행한다. 이를 html 요소에 닿을 때까지 반복한다.
이처럼 이벤트를 등록하면, 그와 연결된 이벤트 타깃과 더불어 DOM 트리와 연결된 이벤트 패스 상에 위치한 모든 DOM 요소에서 이를 캐치할 수 있기 때문에 이벤트 위임처럼 각각의 하위 요소가 아닌 그들의 부모 요소에 이벤트를 위임할 수 있는 것이다.
📝Event.target 과 Event.currentTarget 의 차이!
이번에 프리온보딩 과제에서 라이브러리 없이 dnd 기능을 구현했는데, 이 때 직접 DOM요소에 접근하는 방식으로 구현을 했다. (참고로 전혀 좋은 방식이 아니다. 클래스와 id 를 참조하고 있어서, DOM 요소에 강하게 결합이 되어 있는 문제점이 있다.)
코드를 잠깐 가져와보면,
어떤 부분에서는 e.target으로 접근하고 또 어떤 요소에 대해서는 e.currentTarget으로 접근하고 있다. 참고 코드를 보면서도 구현하기 바빠서 둘의 차이를 제대로 알지 못했는데, 이번에 이벤트 전파에 대해서 공부하다보니 여기서 차이를 알아볼 수 있을 것 같다.
target은 이벤트가 발생한 바로 그 지점을 의미하고,
currentTarget은 캡처링 혹은 버블링에 의해서 이벤트가 실행된 요소를 의미한다.
예를 들어서 두 개의 중첩된 요소 wrapper와 button에 각각 클릭 이벤트 핸들러가 바인딩 되어 있다면, button을 클릭했을 때 target은 내가 클릭한 button, currentTarget은 현재 target 부터 시작해 버블링을 통해 실행된 상위 요소 wrapper를 가리키게 된다.
아래 예제 코드에서 버튼을 클릭한 후 콘솔에 일어나는 일을 확인해보자. target은 그대로 button 이지만, currentTarget이 안에서 바깥쪽 요소로 변하는 것을 볼 수 있다.
📝Event.stopPropagation()
마지막으로 알아볼 것은, 이렇게 이벤트 전파가 일어날 때 특정 지점에서 이벤트가 더이상 전파되지 않도록 하는 Event.stopPropagation() 메서드이다. 여러개의 이벤트 핸들러를 등록할 때 멈추고 싶은 지점에서 이 함수가 호출되도록 하면, 더이상 전파가 일어나지 않는다.
그런데 이 블로그에 따르면, 이러한 메서드를 사용하면 예상치 못한 버그를 발생시킬 위험이 있다고 한다. 따라서 target과 currentTarget을 활용해 currentTarget이 특정 지점에 이르면 버블링이 중단되도록 조건부로 실행하는 방법을 추천하고 있다.
👀참고자료
'JavaScript > JavaScript' 카테고리의 다른 글
ES6의 모듈에 대해서. (0) | 2023.01.26 |
---|---|
[JavaScript] 비동기 흐름 제어 -2 : async / await 활용하기 (0) | 2022.07.28 |
[JavaScript] 비동기 흐름 제어 -1 : 콜백함수, promise (0) | 2022.07.28 |
[JavaScript] 클래스간의 상속 (프로토타입 상속) (0) | 2022.07.25 |
[JavaScript] 프로토타입 체인 (0) | 2022.07.25 |