본문 바로가기
source-code/JavaScript

이벤트 캡처링(Event Capturing)을 통한 이벤트 우선 순위 제어

by mattew4483 2024. 1. 9.
728x90
반응형

이벤트(Event)

이벤트는 사용자의 행동이나 브라우저 자체에서 발생하는 사건을 의미합니다.

모든 DOM node는 특정한 이벤트를 만들어내며, 키보드 입력, 마우스 움직임 등이 대표적인 예시입니다.

 

이벤트 발생 시 브라우저는 이벤트 객체 를 생성합니다.

각 이벤트는 이벤트 객체에 의해 표현되며 해당 객체는 이벤트에 대한 상세한 정보를 포함합니다.

 

이벤트 흐름(Event Flow)

이벤트는 보통 세 단계로 진행됩니다.

  1. 캡처링 단계 (Capture Phase): 이벤트가 최상위(루트) 요소에서 시작하여 대상 요소로 향하는 단계입니다.
  2. 타겟 단계 (Target Phase): 이벤트가 실제 대상 요소에 도달하는 단계입니다.
  3. 버블링 단계 (Bubble Phase): 이벤트가 대상 요소에서 시작하여 최상위(루트) 요소로 거슬러 올라가는 단계입니다.

 

이벤트 버블링(Event Bubbling)과 캡처링(Event Capturing)

이벤트 버블링은 이벤트가 발생한 요소에서 상위 요소로 전파되는 현상을 말합니다.

즉, 이벤트가 가장 구체적인 요소에서 시작해 부모 요소, 조상 요소로 차례로 전파됩니다.

 

이벤트 캡처링은 반대로 이벤트가 가장 상위 요소에서 시작해 구체적인 요소로 전파되는 현상입니다.

이벤트 캡처링은 버블링 전에 발생하며, 상위 요소에서 하위 요소로 이벤트가 전파됩니다.

 

이벤트 캡처링(Event Capturing)을 통한 이벤트 우선순위 제어

대부분의 사례에서는 이벤트 객체의 stopPropagation 메서드를 통해 버블링을 중단합니다.

이유는 간단합니다. 웹 개발을 하다보면 자식 요소의 이벤트만 동작하고, 부모 요소의 이벤트는 동작하길 않길 바랄 때가 잦기 때문이죠.

 

그런데 사내 업무 중, 이벤트 캡처링을 이용해 tricky 하게 문제를 해결한 사례가 있습니다.

요구 사항은 다음과 같았습니다.

1. 사용자가 마우스를 가져다 대면, 해당 요소를 감싼 빨간색 div가 나타난다.
2. 사용자가 해당 요소를 클릭하면, 클릭된 요소의 이벤트 객체를 콜백 함수에 반환한다.

 

구현 자체는 크게 어려울 것이 없었습니다.

document에 mouseover, mouseout 이벤트를 할당한 후,

핸들러에서 반환받은 event.target을 통해 해당 요소의 너비, 높이, 위치를 반영한 div를 생성하면 되니까요.

 

문제는 요구사항 2 → 클릭 시 동작에서 발생했습니다.

// mouseover handler
function handleMouseEnter(event: Event) {
  const target = event.target;

  if (!currentElement) {
    // 빨간 overlay div 생성
    createOverlayDiv(target); 

    // 현재 선택된 element를 ref로 관리 => 할당
    currentElement = target; 

    // 현재 선택된 element에 click 이벤트 추가
    // callback은 클릭이 발생한 event객체를 인자로 받는 함수
    currentElement.addEventListener("click", callback);
  }
}

// mouseleave handler
function handleMouseLeave() {
    // overlay div 제거
    removeOverlayDiv();

    // currentElement내 할당된 click 이벤트 제거
    currentElement?.removeEventListener(
    	"click",
    	callback,
    );
    
    // 초기화
    currentElement = null;
}

// document내 이벤트 할당
document.addEventListener("mouseover", handleMouseEnter);
document.addEventListener("mouseout", handleMouseLeave);

mouseover 이벤트 발생 시 event.target에 click 이벤트를 추가해,

유저가 클릭 이벤트를 실행하면 해당 요소를 인자로 갖는 콜백함수를 호출하도록 했습니다.

 

그런데 문제는... 기존에 해당 요소에 할당된 클릭 이벤트도 같이 동작한다는 점이었습니다!

즉 해당 요소의 클릭 이벤트로 할당된 페이지 이동, alert 창, 모달 등이 모조리 동작하고 말았습니다.

 

물론 정상적인 상황에서는 이것이 아무런 문제가 없지만...

chrome extension으로 제공되는 서비스 특성 상,

기존 이벤트를 모두 막고 저희가 할당한 callback함수만 동작하게 만들 필요가 있었습니다.

 

// 요소 클릭 시 실행될 콜백 함수
function callback(e: Event) {
    // 생략...
    e.preventDefault() // 이벤트 기본 동작 방지
    e.stopPropagation() // 버블링 방지
}

이를 위해 위 두 메서드를 추가했지만, 원하는 대로 동작하지 않았습니다.

 

사실 당연합니다.

해당 요소에 여러 개의 이벤트가 할당되어 있을 경우... 해당 이벤트는 순서대로 실행되니까요.

element.addEventListener("click", () => console.log('1'));
element.addEventListener("click", () => console.log('2'));
element.addEventListener("click", () => console.log('3'));

// console
// 1
// 2
// 3

 

즉, 사용자가 마우스를 가져다 된 순간 해당 요소의 onClick 이벤트 핸들러로 기본 동작과 버블링을 방지해 봤자...

기본에 할당된 이벤트들은 아무런 상관없이 동작하고 마는 것이죠.

 

아하... 따라서 저는

특정 요소에 할당된 이벤트들보다, 항상 먼저 실행될 핸들러를 지정할 방법 이 필요했습니다.

→ 이벤트 캡처링 단계를 이용해 이를 구현할 수 있지 않을까요?

저희가 원하는 callback 함수가 버블링 단계가 아닌 캡처링 단계에서 실행될 경우

다른 핸들러들이 실행되는 event phase보다 빠르기 때문에 → 기본 이벤트 동작이나 다른 핸들러 동작을 막을 수 있을 것입니다!

 

// mouseover handler
function handleMouseEnter(event: Event) {
  // 생략...
  
  // addEventListener의 capture 옵션을 true로 설정
  // 이를 통해 캡처링 단계에서 해당 이벤트를 잡아낼 수 있음
  currentElement.addEventListener("click", callback, true);
}

// mouseleave handler
function handleMouseLeave() {
    // 생략...

    // currentElement내 할당된 click 이벤트 제거
    currentElement?.removeEventListener(
        "click",
        callback,
        true
    );
}

수정된 코드는 다음과 같습니다.

addEventListener의 capture 옵션을 통해, 해당 이벤트를 캡처링 단계에서 실행하도록 변경했습니다.

 

이를 통해 이전에 작성한 preventDefault와 stopPropagation가 가장 먼저 동작하게 되면서,

이후 버블링 단계의 이벤트들의 전파를 막을 수 있었습니다.

→ 요소에 존재하던 어떠한 핸들러들도 실행되지 않고, 오직 callback 핸들러만 동작함을 확인할 수 있습니다!


https://ko.javascript.info/bubbling-and-capturing

 

버블링과 캡처링

 

ko.javascript.info

728x90
반응형