[JavaScript] 이벤트 버블링과 캡처링, 그리고 이벤트 위임
버블링(Bubbling)
한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작한다.
가장 최상단의 조상 요소를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작한다.
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
가장 안쪽의 <p>를 클릭하면 p -> div -> form 순서로 alert창이 뜬다.
이런 흐름을 "이벤트 버블링"이라고 부른다. 이벤트가 제일 깊은 곳에 있는 요소에서 시작해 부모 요소를 거슬러 올라가며 발생하는 모양이 마치 물속 거품(bubble)과 닮았기 때문이다.
거의 모든 이벤트는 버블링된다.
focus 이벤트와 같이 버블링 되지 않는 이벤트도 있다. 버블링 되지 않는 이벤트의 종류에 대해서는 아래에서 설명한다. 몇몇 이벤트를 제외하곤 대부분의 이벤트는 버블링된다.
event.target
부모 요소의 핸들러는 이벤트가 정확히 어디서 발생했는지 등에 대한 자세한 정보를 얻을 수 있다.
이벤트가 발생한 가장 안쪽의 요소는 타깃(target) 요소라고 불리고, event.target을 사용해 접근할 수 있다.
event.target과 this(=event.currentTarget)는 다음과 같은 차이점이 있다.
- event.target은 실제 이벤트가 시작된 target 요소이다. 버블링이 진행되어도 변하지 않는다.
- this는 '현재' 요소로, 현재 실행 중인 핸들러가 할당된 요소를 참조한다.
버블링 중단하기
이벤트 버블링은 타깃 이벤트에서 시작해서 <html> 요소를 거쳐 document 객체를 만날 때까지 각 노드에서 모두 발생한다. 몇몇 이벤트는 window 객체까지 거슬러 올라가기도 한다. 이 때도 모든 핸들러가 호출된다.
그런데 핸들러에게 이벤트를 완전히 처리하고 난 후 버블링을 중단하도록 명령할 수도 있다.
이벤트 객체의 메서드인 event.stopPropagation()을 사용하면 된다.
event.stopImmediatePropagtion()
한 요소의 특정 이벤트를 처리하는 핸들러가 여러 개가 존재하는 상황에서, 핸들러 중 하나가 버블링을 멈추더라도 나머지 핸들러는 여전히 동작한다.
event.stopPropagation()은 위쪽으로 일어나는 버블링은 막아주지만, 다른 핸들러들이 동작하는 건 막지 못한다.
이 때는, event.stopImmediatePropagation()을 사용해야 한다. 이 매서드를 사용하면 요소에 할당된 특정 이벤트를 처리하는 핸들러 모두가 동작하지 않는다.
꼭 필요한 경우를 제외하고는 버블링을 막지 않는다!
버블링을 꼭 멈춰야 하는 명백한 상황이 아니라면 버블링을 막지 말자.
event.stopPropagation()은 추후에 문제가 될 수 있는 상황을 만들어 낼 수 있다.
이벤트 버블링을 막아야 하는 경우는 거의 없으며, 버블링을 막아야 해결되는 문제라면 커스텀 이벤트 등을 사용해 문제를 해결할 수 있다. 핸들러의 event 객체에 데이터를 저장해 다른 핸들러에서 읽을 수 있게 하면, 아래쪽에서 무슨 일이 일어나는지를 부모 핸들러에게 전달할 수 있으므로, 이 방법으로도 이벤트 버블링을 통제할 수 있다.
캡처링(capturing)
표준 DOM 이벤트에서 정의한 이벤트 흐름엔 3가지 단계가 있다.
- 캡처링 단계 : 이벤트가 하위 요소로 전파되는 단계
- 타깃 단계 : 이벤트가 실제 타깃 요소에 전달되는 단계
- 버블링 단계 : 이벤트가 상위 요소로 전파되는 단계
한 element를 클릭하면 이벤트가 최상위 조상에서 시작해 아래로 전파되고(캡처링 단계), 이벤트가 타깃 요소에 도착해 실행된 후(타깃 단계), 다시 위로 전파된다(버블링 단계). 이런 과정을 통해 요소에 할당된 이벤트 핸들러가 호출된다.
캡처링 단계를 이용해야 하는 경우는 흔치 않다.
on<event> 프로퍼티나 HTML 속성, addEventListener(event, handler)를 이용해 할당된 핸들러는 캡처링에 대해 전혀 알 수 없다. 이 핸들러들은 타깃 단계와 버블링 단계에서만 동작한다.
캡처링 단계에서 이벤트를 잡아내려면 addEventListener의 capture 옵션을 true로 설정해야 한다.
elem.addEventListener(..., {capture: true})
// 아니면, 아래 같이 {capture: true} 대신, true를 써줘도 된다.
elem.addEventListener(..., true)
capture 옵션은 두 가지 값을 가질 수 있다.
- false(default) : 버블링 단계에서 동작
- true : 캡처링 단계에서 동작
공식적으로는 총 3개의 이벤트 흐름이 있지만, 이벤트가 실제 타깃 요소에 전달되는 단계인 '타깃 단계'는 별도로 처리되지 않는다. 캡처링과 버블링 단계의 핸들러는 타킷 단계에서 트리거된다.
Form 하위의 div 하위의 P를 클릭하면,
- HTML -> BODY -> FORM -> DIV (캡처링 단계, 첫 번째 리스너)
- P(타깃 단계, 캡처링과 버블링 둘 다에 리스너를 설정했기 때문에 두 번 호출)
- DIV -> FORM -> BODY -> HTML (버블링 단계, 두 번째 리스너)
요약
이벤트가 발생하면 이벤트가 발생한 가장 안쪽 요소가 '타깃 요소(event.target)'가 된다.
- 이벤트는 document에서 시작해 DOM 트리를 따라 event.target까지 내려간다. 이벤트는 트리를 따라 내려가면서 addEventListener(..., true)로 할당한 핸들러를 동작시킨다. addEventListener(..., true)의 true는 {capture:true}의 축약형이다.
- 이후 타깃 요소에 설정된 핸들러가 호출된다.
- 이후에는 이벤트가 event.target부터 시작해서 다시 최상위 노드까지 전달되면서 각 요소에 on<event>로 할당한 핸들러와 addEventListener로 할당한 핸들러를 동작시킨다. addEventListener로 할당한 핸들러 중, 세 번째 인수가 없거나 false, {capture:false}인 핸들러만 호출된다.
각 핸들러는 아래와 같은 event 객체의 프로퍼티에 접근할 수 있다.
- event.target : 이벤트가 발생한 가장 안쪽의 요소
- event.currentTarget (=this) : 이벤트를 핸들링 하는 현재 요소(핸들러가 실제 할당된 요소)
- event.evnetPhase : 현재 이벤트 흐름 단계 (캡처링=1, 타깃=2, 버블링=3)
핸들링에서 event.stopPropagation()을 사용해 이벤트 버블링을 멈출 수 있다.
버블링과 캡처링은 '이벤트 위임(event delegation)'의 토대가 된다.
이벤트 위임은 강력한 이벤트 핸들링 패턴이다.
이벤트 위임
캡처링과 버블링을 활용하면 강력한 이벤트 핸들링 패턴인 이벤트 위임(event delegation)을 구현할 수 있다.
이벤트 위임은 비슷한 방식으로 여러 요소를 다뤄야 할 때 사용된다. 이벤트 위임을 사용하면 요소마다 핸들러를 할당하지 않고, 요소의 공통 조상에 이벤트 핸들러를 단 하나만 할당해도 여러 요소를 한꺼번에 다둘 수 있다.
공통 조상에 할당한 핸들러에서 event.target을 이용하면 실제 어디서 이벤트가 발생했는지 알 수 있으며, 이를 이용해 이벤트를 핸들링한다.
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)
if (!td) return; // (2)
if (!table.contains(td)) return; // (3)
highlight(td); // (4)
};
- elem.closet(selector) 메서드는 elem의 상위 요소 중 selector와 일치하는 가장 근접한 조상 요소를 반환한다. 위 코드에서는 이벤트가 발생한 요소부터 시작해 위로 올라가며 가장 가까운 <td> 요소를 찾는다.
- event.target이 <td> 안에 있지 않으면 그 즉시 null을 반환하므로 아무 작업도 일어나지 않는다.
- 중첩 테이블이 있는 경우 event.target은 현재 테이블 바깥에 있는 <td>가 될 수도 있다. 이런 경우를 처리하기 위해 <td>가 table 범위 안에 있는지 확인한다.
- td를 강조해주는 함수를 실행한다.
'행동' 패턴
이벤트 위임은 요소에 선언적 방식으로 '행동(behavior)'을 추가할 때 사용할 수도 있다. 이 때는 특별한 속성과 클래스를 사용한다.
행동 패턴은 두 부분으로 구성된다.
- 요소의 행동을 설명하는 커스텀 속성을 요소에 추가한다.
- 문서 전체를 감지하는 핸들러가 이벤트를 추적하게 한다. 1에서 추가한 속성이 있는 요소에서 이벤트가 발생하면 작업을 수행한다.
문서 레벨의 핸들러를 만들 때는 항상 addEventListener을 사용하자.
document 객체에 핸들러를 할당할 때는 document.onclick을 사용해서는 안된다.
document.onclick은 충돌을 일으킬 가능성이 있기 때문에 무조건 addEventListner를 사용해야한다. document.onclick은 새로운 핸들러가 이전의 핸들러를 덮어 쓸 수 있다.
이벤트 위임의 장점
- 많은 핸들러를 할당하지 않아도 되기 때문에 초기화가 단순해지고 메모리가 절약된다.
- 요소를 추가하거나 제거할 때 해당 요소에 할당된 핸들러를 추가하거나 제거할 필요가 없기 때문에 코드가 짧아진다.
- innerHTML이나 유사한 기능을 하는 스크립트로 덩어리를 더하거나 뺄 수 있기 때문에 DOM 수정이 쉬워진다.
이벤트 위임의 단점
- 이벤트 위임을 사용하려면 이벤트가 반드시 버블링 되어야 한다. 하지만 몇몇 이벤트는 버블링 되지 않는다. 그리고 낮은 레벨에 할당한 핸들러엔 event.stopPropagation()을 쓸 수 없다.
- 컨테이너 수준에 할당된 핸들러가 응답할 필요가 있는 이벤트이든 아니든 상관없이 모든 하위 컨테이너에서 발생하는 이벤트에 응답해야 하므로 CPU 작업 부하가 늘어날 수 있다. 그런데 이런 부하는 무시할만한 수준이므로 실제로는 잘 고려하지 않는다.
Reference