-
이벤트 버블링(bubbling)JavaScript 2021. 3. 4. 21:11
개발을 하면서 버튼 클릭 이벤트 처리를 하면서 의도하지 않은 결과를 만난적이 있다. 면접 때 질문 받았던 '이벤트 버블링' 이슈였다. 모르면 당황스럽지만, 알면 별 것 아닌 내용이다.
아래 그림과 같이 갈색 div박스, 초록색 div박스, 가장 안쪽에 child라는 버튼을 만들어 각각의 요소들마다 클릭되었을 때 콘솔 창에 event.currentTarget과 event.target을 출력하도록 이벤트를 등록해보자.
... <body> <div class="grand"> <div class="parent"> <button>Child</button> </div> </div> </body> <script> const grand = document.querySelector('.grand'); const parent = document.querySelector('.parent'); const child = document.querySelector('button'); grand.addEventListener('click', event => { console.log(`grand: ${event.currentTarget}, ${event.target}`); }) parent.addEventListener('click', event => { console.log(`parent: ${event.currentTarget}, ${event.target}`); }) child.addEventListener('click', event => { console.log(`child: ${event.currentTarget}, ${event.target}`); }) child.addEventListener('click', event => { console.log('child: another handler') }) </script> ...
가장 안쪽에 있는 Child 버튼을 클릭하면 아래와 같이 Child가 속한 부모와 조상 엘리먼트에 등록된 이벤트까지 호출된다.
이벤트 버블링(bubbling)
위와 같이 동작하는 이유가 자바스크립트의 이벤트 버블링 때문이다. 브라우저에서 한 요소에 이벤트가 발생하면, 우선 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작한다. 그리고 가장 최상단의 조상 요소를 만날 때까지 이 과정이 반복된다.
Child 버튼을 클릭할 때 일어나는 일
- child 버튼에 할당된 핸들러 호출
- parent에 할당된 핸들러 호출
- grand에 할당된 핸들러 호출
이런 동작 방식 때문에 child → parent → grand 순으로 콘솔 로그가 출력되는 것이다. 바로 이런 흐름을 '이벤트 버블링'이라고 한다. 가장 깊은 곳에 있는 요소에서부터 부모 요소를 따라 올라가며 이벤트가 발생하는 모양이 물속의 거품(bubble)과 닮았기 때문이다. 'focus' 이벤트를 비롯해 몇몇 버블링 되지 않는 이벤트도 있지만, 대부분의 이벤트는 버블링 된다.
콘솔 로그로 출력된 event.target은 실제 이벤트가 발생한 요소를 가리킨다. Child 버튼을 클릭했을 때, 버블링에 의해 부모와 조상 요소들에 등록된 핸들러들도 함께 호출되는데, 이때 공통적으로 출력되는 event.target은 실제 클릭 이벤트가 발생한 Child버튼을 가리킨다. event.currentTarget은 현재 실행 중인 핸들러가 할당된, 현재 요소를 가리킨다.(parent와 grand)
버블링을 중단하는 방법
Child를 클릭했을 때 parent와 grand에 등록된 핸들러가 호출되는 것을 원하지 않을 때는 어떻게 해야 할까? 첫 번째 방법은 이벤트 객체의 메서드인 event.stopPropagation()을 사용하는 것이다.
child.addEventListener('click', event => { console.log(`child: ${event.currentTarget}, ${event.target}`); event.stopPropagation() // 이벤트 버블링을 막아준다 // event.stopImmediatePropagation() --> 다른 핸들러가 동작하는 것까지도 막고 싶을 때 사용한다 }) child.addEventListener('click', event => { console.log('child: another handler') })
이렇게 하면 Child 버튼에 등록된 핸들러만 호출되고, 부모 요소로 버블링이 일어나지 않는다. 하지만 Child 버튼에 등록된 다른 핸들러는 여전히 동작하는데, 만약 한 요소에 특정 이벤트를 처리하는 핸들러가 여러 개인데, 다른 핸들러들이 동작하는 것도 막고 싶다면, event.stopImmediatePropagation() 메서드를 사용하면 된다.
두 번째 방법은, 아래와 같이 부모 요소에 핸들러를 등록할 때 event.target과 event.currentTarget이 다를 경우 아무런 동작도 하지 않도록 설계하는 것이다. 첫 번째 방법에서 사용했던 메서드를 사용하면 프로젝트 규모가 커지거나 다른 개발자와 협업할 때 예상치 못한 오류가 생길 수 있기 때문에 이 방법이 조금 더 안전한 방법이라 할 수 있다.
grand.addEventListener('click', event => { if(event.target !== event.currentTarget) { return; } console.log(`grand: ${event.currentTarget}, ${event.target}`); }) parent.addEventListener('click', event => { if(event.target !== event.currentTarget) { return; } console.log(`parent: ${event.currentTarget}, ${event.target}`); })
'JavaScript' 카테고리의 다른 글
객체를 복사할 때 유의할 점 (0) 2021.04.28 반복문 빠져나오기 - break, continue, label (0) 2021.04.22 클로저(closure) (0) 2020.12.07 "use strict" (0) 2020.12.04 호이스팅 (0) 2020.10.28