[JavaScript] 이벤트 버블링(Event Bubbling), 이벤트 캡처링(Event Capturing)
JavaScript

[JavaScript] 이벤트 버블링(Event Bubbling), 이벤트 캡처링(Event Capturing)

728x90

[JavaScript] 이벤트(Event)란? 에서 이벤트(Event)와 이벤트 핸들러(Event Handler)에 대해 알아봤었습니다. 요약하자면 다음과 같습니다.

  1. 이벤트를 감지하는 이벤트 핸들러를 HTML과 연동
  2. 1번에서 연동된 HTML 부분에 사용자가 어떤 행동을 하면 (예: 클릭) 이벤트가 발생
  3. 이벤트 핸들러에 등록된 코드 실행

음.. 근데 만약 HTML 요소들이 중첩되어있다면 이벤트 핸들러는 어떻게 동작할까요? 예를 들어

<html>
  <body>
    <!-- 빨간색 -->
    <div class="depth1" onClick="alert(this.className)">
      <!-- 초록색 -->
      <div class="depth2" onClick="alert(this.className)">
      	<!-- 파란색 -->
        <div class="depth3" onClick="alert(this.className)">
        </div>
      </div>    
    </div>
  </body>
</html>

이 코드에서 depth3 영역을 클릭한다면, 어떤 alert가 뜰까요? 한번 직접 눌러볼까요?

See the Pen Untitled by DannyJae (@jhl8041) on CodePen.

분명 하나의 영역만 클릭했는데, 모든 onClick 이벤트 핸들러가 동작한 것을 확인하셨나요?

위와 같이 중첩된 요소에서 이벤트가 발생할 때는, 이벤트 전파(Event Propagation)가 이루어집니다. 마치 브랜드의 e스킬처럼 말이죠.

롤 브랜드 E 스킬 - 대상이 불타는 상태일때 쓰면 다른 대상에게 화염이 전파된다

이처럼 이벤트가 전파되는 방식에는 이벤트 버블링(Bubbling)이벤트 캡처링(Capturing)이 있습니다. 지금부터 알아볼까요?


이벤트 버블링(Event Bubbling)

버블링의 원리는 간단합니다. 이벤트가 발생한 요소부터 점점 부모 요소를 거슬러 올라가서 window 까지 이벤트를 전파하는 것이죠. 마치 거품(Bubble)이 물 아래에서부터 위로 올라가는 것 처럼요!

 

위에서 살펴본 예제가 바로 버블링에 해당됩니다.

  • depth3 영역을 클릭하면 onClick 핸들러가 동작합니다.
  • 이어서 부모 요소인 depth2의 onClick 핸들러가 동작합니다.
  • 이어서 부모 요소인 depth3의 onClick 핸들러가 동작합니다.

또한 "거의" 모든 이벤트는 버블링 됩니다. focus 이벤트와 같이 버블링 되지 않는 이벤트도 있으니 각 이벤트의 특성을 정확하게 알고 있어야 합니다.

 

그럼 원하지 않아도 항상 버블링을 마주해야하나요?

아닙니다. 버블링을 강제로 중단시키는 방법도 존재합니다. 이벤트 객체의 메서드인 event.stopPropagation()를 사용하면 됩니다.

위 예제에서 depth2에서 버블링을 중단시키도록, event.stopPropagation() 을 추가한 예제입니다.

See the Pen bubbling stop propagation by DannyJae (@jhl8041) on CodePen.

 

event.stopImmediatePropagation()

하지만 한 요소의 특정 이벤트를 처리하는 핸들러가 여러개인 상황에서, 핸들러 중 하나가 버블링을 멈추더라도 나머지 핸들러는 여전히 동작합니다. event.stopPropagation()은 위쪽으로 일어나는 버블링은 막아주지만, 다른 핸들러들이 동작하는 건 막지 못합니다.

버블링을 멈추고, 요소에 할당된 다른 핸들러의 동작도 막으려면 event.stopImmediatePropagation()을 사용해야 합니다. 이 메서드를 사용하면 요소에 할당된 특정 이벤트를 처리하는 핸들러 모두가 동작하지 않습니다.

 

꼭 필요한 경우를 제외하곤 버블링을 막지 마세요!

버블링은 유용합니다. 버블링을 꼭 멈춰야 하는 명백한 상황이 아니라면 버블링을 막지 마세요. 아키텍처를 잘 고려해 진짜 막아야 하는 상황에서만 버블링을 막으세요. event.stopPropagation()은 추후에 문제가 될 수 있는 상황을 만들어낼 수 있습니다.

문제가 발생할만한 시나리오를 살펴봅시다.

  • 중첩 메뉴를 만들었다 가정합시다. 각 서브메뉴(submenu)에 해당하는 요소에서 클릭 이벤트를 처리하도록 하고, 상위 메뉴의 클릭 이벤트 핸들러는 동작하지 않도록 stopPropagation을 적용합니다.
  • 사람들이 페이지에서 어디를 클릭했는지 등의 행동 패턴을 분석하기 위해, window내에서 발생하는 클릭 이벤트 전부를 감지하기로 결정합니다. 분석 시스템을 도입하기로 합니다. 그런데 이런 분석 시스템의 코드는 클릭 이벤트를 감지하기 위해 document.addEventListener('click'…)을 사용합니다.
  • stopPropagation로 버블링을 막아놓은 영역에선 분석 시스템의 코드가 동작하지 않기 때문에, 분석이 제대로 되지 않습니다. 안타깝게도 stopPropagation을 사용한 영역은 '죽은 영역(dead zone)'이 되어버립니다.

이벤트 버블링을 막아야 하는 경우는 거의 없습니다. 버블링을 막아야 해결되는 문제라면 커스텀 이벤트 등을 사용해 문제를 해결할 수 있습니다. 커스텀 이벤트 사용 방법은 추후에 다루겠습니다. 핸들러의 event 객체에 데이터를 저장해 다른 핸들러에서 읽을 수 있게 하면, 아래쪽에서 무슨 일이 일어나는지를 부모 요소의 핸들러에게 전달할 수 있으므로, 이 방법으로도 이벤트 버블링을 통제할 수 있습니다.


이벤트 캡처링(Event Capturing)

버블링은 이벤트가 발생한 요소부터 window 까지 이벤트를 전파했다면, 캡처링은 그 반대입니다. window 부터 이벤트가 발생한 요소까지 이벤트를 전파합니다. 캡처링 단계를 이용해야 하는 경우는 흔하지 않아서, 캡처링에 관한 코드를 발견하는 일은 거의 없을 겁니다. 그래도 종종 유용한 경우가 있으니 알아봅시다.

 

캡처링은 on<event> (예: onClick)나 HTML 속성, addEventListener(event, handler)를 이용해 할당된 핸들러로는 구현할 수 없습니다. 캡처링 단계에서 이벤트를 잡아내려면 addEventListener의 capture 옵션을 true로 설정해야 합니다.

elem.addEventListener(..., {capture: true})
// 아니면, 아래 같이 {capture: true} 대신, true를 써줘도 됩니다.
elem.addEventListener(..., true)

아래 예제는 캡처링 후 버블링을 하도록 설계된 코드입니다.

<html>
  <body>
    <!-- 빨간색 -->
    <div class="depth1">
      <!-- 초록색 -->
      <div class="depth2">
      	<!-- 파란색 -->
        <div class="depth3">
          depth3
        </div>
      </div>    
    </div>
  </body>
  
  <script>
  for(let elem of document.querySelectorAll('*')) {
  	elem.addEventListener("click", e => alert(`[캡쳐링] tag: ${elem.tagName}, class: ${elem.className}`), true);
  	elem.addEventListener("click", e => alert(`[버블링] tag: ${elem.tagName}, class: ${elem.className}`));
  }
  </script>
</html>

See the Pen Capturing and Bubbling example by DannyJae (@jhl8041) on CodePen.

실행해보셨나요? 가장 상단 요소에서 부터 이벤트 전파가 시작이 되었다가, 클릭한 부분까지 오면 다시 버블링이 시작되는 것을 보실 수 있습니다.

 

순서를 조금 더 상세하게 요약하면

  • 이벤트는 document에서 시작해 DOM 트리를 따라 event.target (이벤트가 발생한 가장 안쪽 요소)까지 내려갑니다.
  • 이벤트는 트리를 따라 내려가면서 addEventListener(..., true)로 할당한 핸들러를 동작시킵니다.
  • 이후 타깃 요소(event.target)에 설정된 핸들러가 호출됩니다.
  • 이후엔 이벤트가 event.target부터 시작해서 다시 최상위 노드까지 전달되면서 각 요소에 on<event>로 할당한 핸들러와 addEventListener로 할당한 핸들러를 동작시킵니다. (addEventListener로 할당한 핸들러 중, 세 번째 인수가 없거나 false, {capture: false}인 핸들러만 호출됩니다.)

캡처링은 실무에서 거의 쓰이지 않고, 주로 버블링 단계의 이벤트만 다뤄집니다. 이렇게 된 데는 논리적 배경이 있습니다.

현실에서도 어떤 사고가 발생하면 지역 경찰이 먼저 사고를 조사합니다. 그 지역에 대해 가장 잘 아는 기관은 지역 경찰이기 때문입니다. 추가 조사가 필요하다면 그 이후에 상위 기관이 사건을 넘겨받습니다.

이벤트 핸들러도 이와 같은 논리로 만들어졌습니다. 특정 요소에 할당된 핸들러는 그 요소에 대한 자세한 사항과 무슨 일을 해야 할지 가장 잘 알고 있습니다. 따라서 이벤트를 다룰 기회를 이 요소에 할당된 핸들러에게 가장 먼저 주는 것입니다.

 

 

출처:
- 버블링과 캡처링
- 이벤트 버블링(bubbling)과 캡처링(capturing) :: 마이구미
728x90