이벤트 전파 제어

HyunHo Lee·2022년 1월 12일
7

JavaScript

목록 보기
13/20
post-thumbnail

이벤트 전파란?

웹 서비스를 이용하다가 어떠한 버튼을 클릭했다고 가정해보자. 바닐라 JS로 설계되어 있다면 그 버튼은 기능을 수행하기 위해 document.addEventListener 와 같은 코드를 가지며, 여기서 첫 번째 argument로 'click'과 같은 값이 들어가있을 것이다. 'click'은 이벤트라고 하며 'addEventListener' 은 리스너라고 부른다.


그렇다면 이벤트 전파란 무엇일까? 브라우저 화면에서 이벤트가 발생하면 브라우저는 가장 먼저 이벤트 대상을 찾는다. 이벤트가 발생한 곳을 찾기 위해 DOM Tree에서 가장 상위에 존재하는 window 객체부터 document, html 순으로 내려가는 캡쳐링 단계를 수행한다. 이 과정에서 중간에 만나는 모든 캡처링 이벤트 리스너를 실행한다.

이벤트 대상을 발견하면 타깃 단계를 진행한다. 이벤트가 실제 타깃 요소에 전달되는 과정으로 이벤트가 실행되는 것이다.

그 후 다시 DOM Tree를 따라 올라가는 버블링 단계를 수행하고, 중간에 만나는 모든 버블링 이벤트 리스너를 실행한다. 이 전체적인 과정을 이벤트 전파라고 부른다.

tip ) 브라우저가 웹 페이지를 렌더링 하는 과정에서 HTML을 파싱하며 DOM Tree를 생성한다.


이벤트 전파 제어

버블링

  <body>
    <p>
      일반적인 글
      <span id="prevent">이벤트를 막은 글</span>
      일반적인 글2
    </p>
  </body>
  <script>
    const p = document.querySelector("p");
    p.addEventListener("contextmenu", () => {
      console.log("contextmenu 호출");
    });
  </script>

contextmenu 이벤트는 마우스 오른쪽 클릭을 할 경우 발생한다. 현재 span태그의 이벤트를 막지 않은 상태이므로, p 태그의 어느 부분을 오른쪽 마우스로 클릭해도 contextmenu 호출 이 출력된다.


  <body>
    <p>
      일반적인 글
      <span id="prevent">이벤트를 막은 글</span>
      일반적인 글2
    </p>
  </body>
  <script>
    const p = document.querySelector("p");
    const prevent = document.querySelector("#prevent");
    p.addEventListener("contextmenu", () => {
      console.log("contextmenu 호출");
    });
    prevent.addEventListener("contextmenu", (e) => {
      console.log("이벤트를 막은 contextmenu 호출");
      e.stopPropagation();
      e.preventDefault();
    });
  </script>

이번에는 span 태그에서 stopPropagationpreventDefault으로 이벤트 전파 제어를 해보았다. p태그 내의 일반적인 글을 선택하면 contextmenu 호출이 출력되고 이벤트를 막은 글을 선택하면 이벤트를 막은 contextmenu 호출이 출력되는 것을 볼 수 있다.


stopPropagation

그렇다면 stopPropagation은 어떤 역할을 하는가? 이벤트를 실행하고 버블링을 중단하는 역할을 한다.

버블링은 앞에서 언급한 바와 같이 캡쳐링 단계와 타깃 단계 후에 다시 DOM Tree를 타고 올라가는 단계였다. 이 과정에서 만나는 모든 버블링 이벤트 리스너를 실행하게 되는데 이 예제의 경우 span태그를 우클릭 하면 버블링을 수행하며 p태그를 만나게 될 것이다.

p태그에서는 우클릭 할 경우 contextmenu 호출가 출력되어야 하므로, span태그를 우클릭 하면 이벤트를 막은 contextmenu 호출contextmenu 호출가 함께 출력되야 했다. 하지만 stopPropagation으로 버블링을 중단하여 이벤트를 막은 contextmenu 호출만 출력된 것이다.


preventDefault

우리는 stopPropagation 으로 버블링을 잘 막았다. preventDefault은 무슨 역할을 하는지 궁금하다. 위의 동작에서 일반적인 글이벤트를 막은 글을 우클릭 했을 경우의 동작이 묘하게 다르다.

preventDefault는 브라우저에서 정의한 기본 동작을 제어한다. 우클릭 시 원래 브라우저의 메뉴창이 나타나야한다. 하지만 span 태그에 preventDefault을 넣어 우클릭을 해도 메뉴창이 나타나지 않는다.

MDN 에 나온 예제인 checkbox로 다른 예시를 들어보자. 체크박스는 사용자가 클릭함에 따라 체크가 되고 해제도 된다. 하지만 preventDefault를 주면 당연한 동작이었던 체크 기능을 막는다. 이렇게 이벤트를 제어할 수도 있다.


stopImmediatePropagation

뭔가 뜬금없는 친구가 등장했다. 모양만 봐서는 stopPropagation의 업그레이드 버전같기도 하다. 정체가 뭘까?

  <body>
    <p>아무데나 클릭하세요.</p>
  </body>
  <script>
    addEventListener("click", () => console.log(1));
    addEventListener("click", () => console.log(2));
    addEventListener("click", () => console.log(3));
  </script>

클릭 이벤트가 발생하면 1 2 3 을 출력하고 있다. 원하는 동작이 첫 번째 addEventListener 까지만 수행되고 나머지는 무시하고 싶다. 이 경우에 사용하는 것이 stopImmediatePropagation 이다.


  <body>
    <p>아무데나 클릭하세요.</p>
  </body>
  <script>
    addEventListener("click", (e) => {
      e.stopImmediatePropagation();
      console.log(1);
    });
    addEventListener("click", () => console.log(2));
    addEventListener("click", () => console.log(3));
  </script>

첫 번째 addEventListenerstopImmediatePropagation을 넣어주어 이 뒤에 실행 예정이었던 이벤트 리스너들을 모두 무시한다. 주의할 점은 stopImmediatePropagation이벤트 전파를 제어하는 것이 아니라 캡쳐링과 버블링을 포함해 다른 모든 이벤트의 실행을 막는 것이다. (최상단에서 사용해버리면 해당 이벤트 리스너 실행되고 이벤트 전파가 끝나버림)

stopImmediatePropagation 참고


캡쳐링

addEventListener(event, handler)

캡쳐링에 대해서도 동작을 확인해 볼 수 있다. 보통 우리가 사용하는 이벤트 리스너는 이와 같다. 이런 경우 이벤트의 동작은 타깃 단계와 버블링 단계에서만 수행된다.

addEventListener(event, handler, true)

3번째 argument의 기본 값은 false로 버블링 단계에서 수행한다는 의미이다. 캡쳐링 단계에서 수행하고 싶다면 true로 설정해야 한다.


  <body>
    <form>
      FORM
      <div>
        DIV
        <p>P</p>
      </div>
    </form>
  </body>
  <script>
    for (let elem of document.querySelectorAll("*")) {
      elem.addEventListener(
        "click",
        (e) => console.log(`캡쳐링: ${elem.tagName}`),
        true
      );
      elem.addEventListener("click", (e) =>
        console.log(`버블링: ${elem.tagName}`)
      );
    }
  </script>

이 코드는 모든 요소를 가져와 캡쳐링과 버블링이 어떻게 진행되고 있는지 보기 위한 코드이다.

p 태그를 클릭하게 되면, querySelectorAll("*")로 모든 요소를 가져온다. 그리고 for문을 시작한다.

가장 먼저 가져오는 요소는 HTML이다. 첫 번째 이벤트 리스너를 통해 캡쳐링 : HTML 이 출력되고 버블링은 아직 하지 않기 때문에 밑에 리스너는 무시한다. 그 후 BODY, FORM, DIV도 마찬가지다.

p태그의 경우는 클릭 되었으니 2개의 이벤트 리스너가 모두 실행되어 캡쳐링 : p버블링 : p가 출력된다. 여기서 우리는 이벤트 리스너가 순서에 맞게 실행된다는 것도 알 수 있다.

이제 타깃 단계까지 수행했으니 버블링을 순서대로 수행하면 끝이다.

모던 자바스크립트 튜토리얼 참고

profile
함께 일하고 싶은 개발자가 되기 위해 달려나가고 있습니다.

2개의 댓글

comment-user-thumbnail
2022년 1월 12일

우와 면접 준비로 딱 좋은 글이네요!

1개의 답글