Event Capturing & Bubbling

임철종·2022년 7월 11일
2
post-thumbnail

이벤트가 일어나는 곳이 겹쳐있다면?

예시

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <link rel="stylesheet" href="https://unpkg.com/mvp.css">
  <title>Document</title>
  <style>
      .outer {
          width: 500px;
          height: 500px;
          background-color: orange;
      }

      .inner {
          width: 50%;
          height: 50%;
          margin: auto;
          background-color: green;
          transform: translateY(50%);
      }

      button {
          position: relative;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          color: white;
      }
  </style>
</head>
<body>
<div class="outer">
  <div class="inner">
    <button>Click Me</button>
  </div>
</div>
<script>
    const outer = document.querySelector('.outer');
    const inner = document.querySelector('.inner');
    const button = document.querySelector('button');

    outer.addEventListener('click', event => {
        console.log(`outer: ${event.currentTarget}, ${event.target}`);
    });
    inner.addEventListener('click', event => {
        console.log(`inner ${event.currentTarget}, ${event.target}`);
    });
    button.addEventListener('click', event => {
        console.log(`button1 ${event.currentTarget}, ${event.target}`);
    });
</script>
</body>
</html>


간단한 예시를 만들어 보았습니다.
주황색 박스(outer) 안에 녹색 박스(inner)가 있고 그 안에 파란색 버튼이 있습니다.
세 요소 모두 click 이벤트가 등록되어 있습니다.

각 요소를 클릭하면 이벤트가 발생하고 콘솔에 지정한 텍스트가 나옵니다.
이때, 안쪽 요소들인 innerbutton이 클릭되면 그 바깥쪽(부모)요소의 클릭 이벤트도 발생시킵니다.


가장 안쪽의 버튼을 클릭하니 outerinner의 이벤트가 발생한 것을 볼 수 있습니다.
왜 그럴까요?


DOM Event의 흐름

MDN을 보면, 이벤트 흐름에는 3가지 단계가 있습니다.
1. Capturing 단계 - 이벤트가 하위 요소로 전파되는 단계
2. Target 단계 - 이벤트가 실제 타겟 요소에 전달되는 단계
3. Bubbling 단계 - 이벤트가 상위 요소로 전파되는 단계

Capturing

<td>가 클릭되면 이벤트가 최상위 요소로부터 시작해 아래로 전파됩니다.
이 단계를 capture 단계라고 합니다.

on<event>속성이나 addEventListener()를 이용해 할당된 핸들러는 capturing에 대해서 전혀 알 수 없습니다. 이 핸들러들은 target 단계나 bubbling 단계에서만 동작합니다.

capturing 단계에서 이벤트를 잡아내려면 addEventListener(...,{capture: true}) 혹은 addEventListener(..., true)capture 옵션을 true로 설정해야 합니다.

하지만 capture 단계를 이용하는경우는 거의 없기 때문에 알아만 두고 넘어가겠습니다.

Target

이벤트가 타겟 요소에 도착해 실행됩니다.

공식적으로는 세개의 단계가 있지만, 실제로 타겟 요소에 전달되는 단계인 target 단계는 별도로 처리되지 않습니다.
capturing과 bubbling 단계의 핸들러는 target 단계에서 실행됩니다.

Bubbling

이벤트가 다시 상위 요소로 전파됩니다.
이 과정에서 각 상위 요소에 할당된 이벤트 핸들러가 동작합니다.

이런 흐름을 Event Bubbling이라고 부릅니다.
마치 물 속의 거품이 위로 올라가듯 깊은 곳(자식 요소) 부터 높은 곳(부모 요소)까지 올라가며 발생하기 때문입니다.

거의 모든 이벤트에서 버블링이 발생하지만 focus처럼 버블링 되지 않는 이벤트도 있습니다.


이를 알고 다시 예시를 보면

button이 클릭되어 이벤트가 발생하면, bubbling이 일어나면서 inner의 핸들러를 실행하고 다음으로 outer의 핸들러를 실행한다는 것을 알 수 있습니다.

event객체

targetcurrentTarget의 차이는 콘솔에 찍힌 event.targetevent.currentTarget을 보면 알 수 있습니다.

  • event.target - 이벤트가 발생한 가장 안쪽의 요소
  • event.currentTarget (this) - 이벤트를 핸들링 하는 현재 요소 (핸들러가 실제 할당된 요소)
  • event.eventPhase - 현재 이벤트의 단계 (1=capture, 2=target, 3=bubbling)

주의할 점

이벤트 버블링을 막기 위해서 event.stopPropagation()을 사용하면 event.currentTarget에서만 이벤트가 발생하고 bubbling이 일어나지 않습니다.
만약 같은 요소에 여러 리스너가 등록되어 있고 특정 리스너 이후의 이벤트를 막고 싶다면 event.stopImmetiatePropagation()을 사용하면 사용한 이후의 이벤트는 핸들링하지 않습니다.

하지만! 이렇게 bubbling을 막아버리면 추후에 버블링이 필요한 경우가 생긴다면 예상치 못한 문제가 발생할 수 있기 때문에 추천하지 않는 방법입니다.

한가지 방법으로 조건을 거는 방법이 있습니다.

<script>
    const outer = document.querySelector('.outer');
    const inner = document.querySelector('.inner');
    const button = document.querySelector('button');

    outer.addEventListener('click', event => {
        if (event.target !== event.currentTarget) {
            return;
        }
        console.log(`outer: ${event.currentTarget}, ${event.target}`);
    });
    inner.addEventListener('click', event => {
        if (event.target !== event.currentTarget) {
            return;
        }
        console.log(`inner ${event.currentTarget}, ${event.target}`);
    });
    button.addEventListener('click', event => {
        console.log(`button1 ${event.currentTarget}, ${event.target}`);
    });
</script>

이런식으로 event.targetevent.currentTarget이 다를 때는 처리하지 않도록 하면
button이 클릭되었을 때는 button의 핸들러만 실행하게 됩니다.

Capture 단계는 왜 쓰이지 않는가?

현실의 논리로 비유해보겠습니다.
어떠한 사건이 발생하게 되면 그 지역 경찰이 먼저 사건을 조사하게 됩니다.
그 지역을 관할하고 있고 그 지역에 대해 잘 알고 있기 때문이죠.

하지만 추가적인, 더 큰 범위의 조사가 필요하게 되면 그보다 큰 지역을 관할하는 부서로 사건을 넘기게 됩니다.

이벤트 핸들러도 이와 같은 논리로 만들어졌습니다.

특정 요소에 할당된 핸들러는 그 요소에 대한 모든 것을 알고 있기 때문에 이벤트(사건)가 발생하면 위쪽부터 내려가며 탐색하여 가장 먼저 그 요소를 다룰 기회를 얻게 됩니다.
그것이 더 많은 정보를 가지고 올 수 있기 때문이죠.

그렇기 때문에 주로 관할 경찰부터 시작하여 위로 올라가는 bubbling 단계를 주로 다루게 되는 것입니다.


참고 - 모던 JavaScript 튜토리얼, MDN

profile
🌑🌘🌗🌖🌕

0개의 댓글