프로그래머스 데브코스에서 노션 클로닝 프로젝트를 진행하면서, 특정 item 내의 여러 버튼들마다 각각 다른 기능을 하도록 구현해야하는 상황이 있었다. item이 동적으로 추가될 때마다 버튼들도 추가 되기 때문에, 버튼이 추가될 때마다 각 버튼에 이벤트 리스너를 하나하나 등록해주는 것은 굉장히 번거롭다고 생각했다.
이런 상황에 사용할 수 있는 것이 바로 이벤트 위임이다. 이벤트 버블링과 캡처링을 통해 어떻게 이벤트 위임을 구현할 수 있는지 알아보자 !
우리가 특정 요소를 클릭했을 때, click 이벤트는 딱 한 번 발생하는 것 같이 보이지만, 사실은 그렇지 않다.
이벤트가 특정 element에서 발생될 경우, 해당 이벤트는 3단계를 거쳐서 전달된다.
<td>
element를 클릭했다면
window
객체에서 시작해 하위로 전파되고td
객체에 도착해 실행된 후 window
객체에게까지 전파된다. 우리가 일반적으로 자주 사용하는 방식인, 특정 이벤트에 대해 html 태그 onClick
속성이나 addEventListener()
함수로 등록한 리스너는 Bubbling 단계에서 발생한 이벤트를 감지하고, 이에 대한 액션을 취해준다.
<element onclick="...">
element.addEventListener("click", (e) => {
...
})
예를 들어 다음과 같은 구조로 element들이 구성되어 있고, 여기서 input 태그를 클릭한다면,
<body onclick="console.log(`body click`)"> body
<div id="outer_div" onclick="console.log(`outer_div click`)"> outer_div
<div id="inner_div" onclick="console.log(`inner_div click`)"> inner_div
<input id="input" type="text" onclick="console.log(`input_click`)" value="input">
</div>
</div>
</body>
Bubbling 단계에서 클릭 이벤트가 input => inner_div => outer_div => body 순서로 전달될 것이고, 콘솔 창에서도 이를 확인할 수 있다.
이벤트 Capture 단계는 최상위 부모인 window element에서부터 이벤트를 발생시킨 특정 element 까지 전달된다.
Capture 단계의 이벤트를 감지하고, 이에 대한 Listener를 실행하고 싶다면 addEventListener()
함수의 세번째 인자로 true
값 또는 {capture: true}
를 전달해주면 된다.
<body id="body"> body
<div id="outer_div"> outer_div
<div id="inner_div"> inner_div
<input id="input" type="text" value="input"/>
</div>
</div>
</body>
<script>
const body = document.querySelector("#body");
const outer_div = document.querySelector("#outer_div");
const inner_div = document.querySelector("#inner_div");
const input = document.querySelector("#input");
body.addEventListener("click", (e) => {
console.log(`Capture event : body`);
},
true
);
outer_div.addEventListener("click", (e) => {
console.log(`Capture event : outer_div`);
},
true
);
inner_div .addEventListener("click", (e) => {
console.log(`Capture event : inner_div `);
},
true
);
input.addEventListener("click", (e) => {
console.log(`Capture event : input`);
},
true
);
</script>
이렇게 각 태그에 Capture event에 대한 Listener를 등록하고 input element를 클릭하면, 부모에서부터 이벤트를 발생시킨 element인 input까지 순서대로 이벤트가 전달되는 것을 확인할 수 있다.
이벤트 캡처링 또는 버블링이 일어났을 때, 이벤트를 최초로 발생시킨 element를 Target element라고 라고 한다. event.target
을 통해 접근이 가능하다.
ex) 버튼을 클릭했을 때, 버튼 element가 Target element가 된다.
현재 이벤트가 발생된 element를 가리키고, event.currentTarget
으로 접근이 가능하다.
body 태그에 Capture 이벤트와 Bubbling 이벤트에 대한 Listener를 둘 다 등록하고, 특정 element가 클릭되었을 때 그 이벤트에 대한 e.target
과 e.currentTarget
을 확인해보자.
<body id="body"> body
<div id="outer_div"> outer_div
<div id="inner_div"> inner_div
<input id="input" type="text" value="input"/>
</div>
</div>
</body>
<script>
const bodyElement = document.querySelector("body");
bodyElement.addEventListener("click", (e) => { // Capture event
console.log(`Capture event => e.target: ${e.target.id}, e.currentTarget: ${e.currentTarget.id}`)
}, true);
bodyElement.addEventListener("click", (e) => { // Bubble event
console.log(`Bubble event => e.target: ${e.target.id}, e.currentTarget: ${e.currentTarget.id}`)
});
</script>
빨간색 박스인 inner_div
를 클릭했다면
우선 Capture event 단계에서 body에서 클릭 이벤트를 감지할 것이고
e.target : inner_div
클릭 이벤트를 발생시킨 최초의 element는 inner_div
이기 때문에
e.currentTarget : body
inner_div
로 부터 발생된 이벤트가 현재 Capture 단계에서 전달되면서 현재는 body element에서 이벤트가 발생했기 때문에
Bubbling event 단계에서도 body에서 클릭 이벤트를 감지할 것이다.
e.target : inner_div
클릭 이벤트를 발생시킨 최초의 element는 여전히 inner_div
이기 때문에
e.currentTarget : body
inner_div
로 부터 발생된 이벤트가 현재 Bubbling 단계에서 전달되면서 현재는 body element에서 이벤트가 발생했기 때문에
이벤트 위임이란, 여러 element 마다 이벤트 리스너를 등록하지 않고, 이벤트 캡쳐링과 버블링을 이용해 공통되는 부모 element에 이벤트 리스너를 등록해 이벤트를 관리하는 방식이다.
위에서 살펴보았듯이, 특정 element에 대한 클릭 이벤트가 발생하면 이벤트 버블링을 통해 그 element의 부모에게까지 이벤트가 전달되기 때문에, 부모 element에 리스너를 등록하면 부모 elemet의 자식 element에 대한 이벤트들을 관리할 수 있는 것이다.
다시 처음으로 돌아가서, 다음과 같이 여러가지 버튼들에 대한 리스너를 등록해주고 싶을 때 이벤트 위임을 사용하는 법을 알아보자.
전체적인 구조는 documentListElement 하위에 여러 documentItem들이 있고, 그 안에 여러 button들이 있는 구조이다.
e.target
을 통해 이벤트를 발생시킨, 즉 클릭이 발생한 button의 className을 가져온다.e.target.closest
메서드를 통해, button의 부모 element인 documentItem에 접근할 수 있고, documentItem의 id에 접근해 이에 따라 원하는 작업을 실행할 수 있다.동적인 엘리먼트에 대한 이벤트 처리가 수월하다.
=> 상위 엘리먼트에서만 이벤트 리스너를 관리하기 때문에 하위 엘리먼트는 자유롭게 추가 삭제할 수 있다.
이벤트 핸들러 관리가 쉽다.
=> 동일한 이벤트에 대해 한 곳에서 관리하기 때문에 각각의 엘리먼트를 여러 곳에 등록하여 관리하는 것보다 관리가 수월하다.
메모리 사용량이 줄어든다.
=> 동적으로 추가되는 이벤트가 없어지기 때문에 당연한 결과이다. 1000건의 각주를 등록한다고 생각해보면 고민할 필요도 없는 일이다.
메모리 누수 가능성도 줄어든다.
=> 등록 핸들러 자체가 줄어들기 때문에 메모리 누수 가능성도 줄어든다.
민우님은 설명을 정말 잘하시는 것 같습니다. 모각코 때도 느꼈지만 이해가 참 잘 됩니다👍