웹 서비스를 이용하다가 어떠한 버튼을 클릭했다고 가정해보자. 바닐라 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 태그에서 stopPropagation
과 preventDefault
으로 이벤트 전파 제어를 해보았다. p태그 내의 일반적인 글
을 선택하면 contextmenu 호출
이 출력되고 이벤트를 막은 글
을 선택하면 이벤트를 막은 contextmenu 호출
이 출력되는 것을 볼 수 있다.
그렇다면 stopPropagation
은 어떤 역할을 하는가? 이벤트를 실행하고 버블링을 중단하는 역할을 한다.
버블링은 앞에서 언급한 바와 같이 캡쳐링 단계와 타깃 단계 후에 다시 DOM Tree를 타고 올라가는 단계였다. 이 과정에서 만나는 모든 버블링 이벤트 리스너를 실행하게 되는데 이 예제의 경우 span태그를 우클릭 하면 버블링을 수행하며 p태그를 만나게 될 것이다.
p태그에서는 우클릭 할 경우 contextmenu 호출
가 출력되어야 하므로, span태그를 우클릭 하면 이벤트를 막은 contextmenu 호출
과 contextmenu 호출
가 함께 출력되야 했다. 하지만 stopPropagation
으로 버블링을 중단하여 이벤트를 막은 contextmenu 호출
만 출력된 것이다.
우리는 stopPropagation
으로 버블링을 잘 막았다. preventDefault은 무슨 역할을 하는지 궁금하다. 위의 동작에서 일반적인 글
과 이벤트를 막은 글
을 우클릭 했을 경우의 동작이 묘하게 다르다.
preventDefault는 브라우저에서 정의한 기본 동작을 제어한다. 우클릭 시 원래 브라우저의 메뉴창이 나타나야한다. 하지만 span 태그에 preventDefault을 넣어 우클릭을 해도 메뉴창이 나타나지 않는다.
MDN 에 나온 예제인 checkbox로 다른 예시를 들어보자. 체크박스는 사용자가 클릭함에 따라 체크가 되고 해제도 된다. 하지만 preventDefault
를 주면 당연한 동작이었던 체크 기능을 막는다. 이렇게 이벤트를 제어할 수도 있다.
뭔가 뜬금없는 친구가 등장했다. 모양만 봐서는 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>
첫 번째 addEventListener
에 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
가 출력된다. 여기서 우리는 이벤트 리스너가 순서에 맞게 실행된다는 것도 알 수 있다.
이제 타깃 단계까지 수행했으니 버블링을 순서대로 수행하면 끝이다.
우와 면접 준비로 딱 좋은 글이네요!