이벤트 위임
유사한 노드들을 다뤄야 할 때 노드마다 핸들러를 할당하지 않고,
공통 조상에 이벤트 핸들러를 할당하여 여러 노드들을 한꺼번에 다루는 방법이다.
요약하면, 이벤트를 효율적으로 다루는 방법이다.
이벤트가 전파되는 특징(캡처링과 버블링)을 이용하면 이벤트 위임을 구현할 수 있다.
이벤트 위임을 배우기에 앞서, 먼저 노드마다 직접 핸들러를 할당하는 경우를 살펴보자.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
const lists = document.querySelectorAll('li');
function handleClick(event) {
console.log(event.target.innerHTML);
}
lists.forEach((li) => li.addEventListener('click', handleClick));
</script>
</body>
</html>
지금은 나름대로 querySelectorAll + forEach를 통해 그럴싸한 코드를 작성해뒀는데,
여기서 add 버튼을 통해 li태그가 추가되거나, li태그에 delete 버튼을 생성해 삭제버튼을 도입했다고 하자.
새로운 li태그 추가시에 해당 태그에도 이벤트핸들러를 부여해야 하며,
기존 li태그 삭제시에 해당 태그에 있던 이벤트핸들러를 해지해야겠다.
만약 지금 같은 단순한 숫자 리스트가 아닌, 쇼핑몰에서 각각의 상품들이 list로서 추가된다면?
쇼핑몰의 상품들이 몇십개정도로 끝날까?
그래서 이런 경우에는 이벤트 위임을 적용해보면 좀 더 좋을 듯 하다.
event.currentTarget, event.target
이벤트 위임을 위해서는, 이벤트핸들러에서 감지한 이벤트 객체를 이용할 필요가 있다.
currentTarget은 현재 타겟(=이벤트를 감지한 현재 위치)를 의미한다.
target은 이벤트가 처음 발생한 위치를 의미한다.
그래서 공통 조상의 이벤트핸들러에서 event.target을 이용하면, 이벤트가 발생한 위치를 파악할 수 있게 된다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
const ul = document.querySelector('ul');
ul.addEventListener('click', (event) => {
const { currentTarget, target } = event;
console.log(currentTarget);
console.log(target);
});
</script>
</body>
</html>
위의 예제와 비슷하나, li 태그들의 공통 부모 태그인 ul에만 이벤트핸들러를 부여했다.
그러면 이벤트핸들러에서 event 객체의 target 프로퍼티를 통해 어디서 이벤트가 발생했는지를 알 수 있게 되고
위에서 일일히 li 태그에 이벤트핸들러를 부여하던 방식관 달리, li태그의 추가와 삭제에도 유리한 구조를 가지게 된다.
그렇다면 우리가 분명 li를 클릭했으나, 왜 ul의 이벤트핸들러에서 이벤트가 감지된 것일까?
이벤트 전파
항상 이벤트는 "전파"되는 특성이 있다.
우선, 방금 우리는 li 태그를 클릭했지만,
li 태그 클릭은 ul 태그를 클릭한 것이고,
ul 태그 클릭은 body 태그 클릭이며,
body 태그는 html 태그를 누른 것이고
html 태그를 누른 것은 document 객체에서 감지되며
document 객체에서 감지된 이벤트는 window 객체에서도 감지되게 된다.
??????????????????????응? 아무튼 그렇다.
이벤트는 3가지 단계를 거치게 되는데,
캡처링 단계, 타겟 단계, 버블링 단계로 나뉜다.
그림을 통해 위에서 설명했던 li..ul..body..주저리주저리가 타겟 단계 + 버블링 단계를 의미함을 알 수 있다.
저렇게 설명한 이유는, elem.addEventListener()의 기본 설정이 이벤트 버블링 감지이기 때문이다.
예제를 통해서 확인해봐도 결과는 같다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
<li>1</li>
</ul>
<script>
const { body } = document;
const ul = document.querySelector('ul');
const li = document.querySelector('li');
function handleClick(pos) {
return () => {
console.log(`I'm ${pos}`);
};
}
document.addEventListener('click', handleClick('document'));
body.addEventListener('click', handleClick('body'));
ul.addEventListener('click', handleClick('ul'));
li.addEventListener('click', handleClick('li'));
</script>
</body>
</html>
그럼 이제 버블링과 캡처링이 무엇이며 어떻게 다뤄야 하는 것인지 알아보자.
이벤트 버블링, 이벤트 캡처링
이벤트 버블링
이벤트가 발생한 위치(target)을 기준으로, 그것을 감싼 상위 엘리먼트까지 올라가면서 이벤트가 전파되는 현상
그래서 target(li)를 기준으로 ul > body > document...로 이벤트가 전파된다.
이벤트 버블링을 감지하기 위해서는, 감지하고 싶은 태그에서 addEventListener로 이벤트핸들러를 달아주면 된다.
(focus같이 전파되지 않는 이벤트가 있으나, 이는 예외다.)
이벤트 캡처링
window부터 시작해서 이벤트가 발생한 위치(target)까지 이벤트가 전파되는 현상
그래서 li를 클릭하면, window > document > body > ul > li로 이벤트가 전파된다.
이벤트 캡처링을 감지하기 위해서는, 감지하고 싶은 태그에서 addEventListener의 3번째 인수로 true 값을 부여한다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
<li>1</li>
</ul>
<script>
const { body } = document;
const ul = document.querySelector('ul');
const li = document.querySelector('li');
function bubbling(pos) {
return () => {
console.log(`bubble ${pos}`);
};
}
document.addEventListener('click', bubbling('document'));
body.addEventListener('click', bubbling('body'));
ul.addEventListener('click', bubbling('ul'));
li.addEventListener('click', bubbling('li'));
function capturing(pos) {
return () => {
console.log(`capture ${pos}`);
};
}
document.addEventListener('click', capturing('document'), true);
body.addEventListener('click', capturing('body'), true);
ul.addEventListener('click', capturing('ul'), true);
li.addEventListener('click', capturing('li'), true);
</script>
</body>
</html>
조금 헷갈릴 수 있는데, 정리하면 다음과 같다.
이벤트는 capturing phase -> target phase -> bubbling phase로 전파된다.
addEventListener에 부여한 핸들러는, 기본적으로 bubbling phase를 감지한다.
addEventListener의 3번째 인수로 true를 부여하면, capturing phase를 감지한다.
그래서 이와 같이 이벤트가 전파되는 특성을 통해서, 이벤트 위임을 적용할 수 있다.
번외) 이벤트 전파를 막는 방법
event.stopPropagation 메서드를 이용한다.
이벤트 캡처링 / 버블링을 감지하고, 현재 감지한 이벤트 이후의 전파를 막게 된다.
event.preventDefault와 혼동될 수 있는데, 이것은 해당 이벤트의 기본 동작을 막는다.
stopPropagation은 부모/자식 엘리먼트로의 이벤트가 전파되는 것을 막는다.
그래서 만약 document + capturing에서의 핸들러에서 preventDefault를 이용하면,
이벤트는 기본 동작을 안할 뿐 하위에 전파되어 body, ul 등등에서 감지된다.
그러나 동일한 조건에서 stopPropagation을 활용하면 capturing 단계에서 document에서 이벤트 전파가 막혀서
capturing body, ul 등등이 감지되지 않을 뿐 아니라, bubbling li, ul 등등도 감지되지 않게 된다.
이벤트 전파가 capturing 단계에서 document 레벨에서 막혔기 때문이다.
물론 이벤트 전파는 정말 특별한 이유가 있는게 아니라면 막지 않는 것이 좋다.
그 태그를 정확히 클릭하지 않았을 수도 있다.
우리가 분명 li를 클릭했다고 생각했지만, 사실 li가 아니라 li태그 안의 img를 눌렀다거나, span을 눌렀을 수도 있다.
이 때는 event.target에 li가 나타나지 않으므로 이벤트위임을 이용하는 핸들러가 예상과 다른 동작을 할 수도 있다.
element.closest(selector)
closest 메서드는 자신(element)을 기준으로, selector를 찾을 때 까지 부모방향으로 탐색한다.
selector로 tag, class, id 등등을 이용할 수 있으며, 탐색하며 처음 발견한 selector를 반환하게 된다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
<li>1<span>Hi I'm span</span></li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
const ul = document.querySelector('ul');
ul.addEventListener('click', (event) => {
const { target } = event;
const li = target.closest('li');
console.log(target);
console.log(li);
});
</script>
</body>
</html>
이렇게 되면 event.target은 span이며, closest로 li를 찾을 수 있다.
요약
이벤트 위임은, 다음과 같은 순서로 구현한다.
1. 감지할 노드들의 상위 노드에 핸들러를 할당한다.
2. 핸들러에서 event.target을 통해 이벤트가 발생한 위치를 알아낸다.
3. 원하는 노드에서 이벤트가 발생했다면, 핸들링해주면 된다.
그래서 장점과 단점을 정리해보면
장점
공통 조상에 핸들러를 할당하여, 초기화가 단순해지며 이벤트핸들러로 인한 메모리도 절약된다.
하위 노드 추가 혹은 제거시 노드에 할당된 핸들러를 추가/제거할 필요가 없다.
하위 노드에 부여한 data 속성이나 closest메서드 등등을 이용해 이벤트 핸들링을 응용할 수 있다.
단점
반드시 이벤트가 전파(특히 버블링)되어야 한다.
상위 노드에 할당한 이벤트핸들러는, 모든 하위 노드에서 버블링된 이벤트에 응답해야한다.
'JavaScript > theory' 카테고리의 다른 글
[Array] 일반적인 배열과 자바스크립트의 배열 알아보기 (0) | 2022.10.13 |
---|---|
엄격모드. "use strict" (0) | 2022.07.20 |
[함수형] 커링 Currying을 배워보자. (0) | 2022.06.01 |
[자바스크립트] 마이크로 태스크 큐의 비동기 작업 처리와 렌더링 시점을 알아보자. (1) | 2022.04.14 |
브라우저/노드 환경에서 모듈, AMD, CommonJS, UMD 알아보기 (1) | 2022.02.15 |