Santos의 개발블로그

stopPropagation vs stopImmediatePropagation 본문

Language & Framework & Library/JavaScript

stopPropagation vs stopImmediatePropagation

Santos 2023. 11. 11. 17:55

 

사내에서 개발 중인 여러가지 기능 중 이벤트 전파를 중단 시켜야 하는 상황이 생기게 되었다. 한 줄의 코드라도 코드의 대한 명분이 확실해야 한다는 것이 모든 개발자가 생각하는 중요한 것 중 하나이지만, 막상 실무에서 빠르게 자신의 할당량을 채워내기 바쁜 상황이면, 크롬 탭에 새로운 친구( 아.. 좀 이따가 읽어봐야지 하는 개발 관련 글 )를 만드는 현실이다. 이러한 상황들을 조금이라도 무마하기 위해, 또 습득한 지식을 머리 속에 남기기 위해 정리하는 시간을 갖고자 한다. 

잘 알고 쓰면 득이 되고, 그렇지 않으면 독이 되는 이벤트 전파와 그에 관련된 기능 stopPropagation, stopImmediatePropagation의 기본적인 설명서에 대한 기록이다.


StopPropagation

대부분의 DOM 엘리먼트는 버블링과 캡쳐링을 지원한다. 그래서 각 엘리먼트에서 이벤트가 발생하는 경우 최상위 엘리먼트인 window에서 이벤트가 발생한 엘리먼트까지 모두 방문을 한다.  

 

여기서 캡처링과 버블링에 대해 간단하게 짚고 넘어가자.

캡처링: 파란색 배경인 td 엘리먼트에서 이벤트가 발생하였을 때, 최 상위에서 실제 이벤트가 걸린 e.target 까지 내려오는 과정을 캡처링이라 한다.
버블링: 반대로 실제 이벤트가 발생한 엘리먼트부터 (td) 최 상위까지 위로 올라가는 과정을 버블링이라 한다. 

 

e.target: 실제 이벤트가 걸린 DOM 엘리먼트
e.currentTarget: 중간에 방문하는 DOM 엘리먼트

 

이벤트 핸들러를 등록할 때 버블링이 기본값으로 셋팅 되어 있으므로, 캡처링 단계까지만 실행되기를 바란다면 아래처럼 등록을 해야 한다. 

window.addEventListener('click', log, true);
// 혹은 아래처럼
window.addEventListener('click', log, {
  capture: true
});

 

간혹 캡처링과 버블링에 대해 혼동이 오는 경우들이 있다. "캡처링 이벤트가 다른 이벤트 핸들러보다 먼저 실행이 된다" 라는 허무맹랑한 이야기들이 오고가는 등등.. 하지만 자바스크립트는 이벤트 핸들러 리스트를 가지고 콜백의 실행 순서를 결정하기 때문에 만약 이전에 등록된 다른 이벤트 핸들러가 있다면, 등록된 이벤트 핸들러 실행 후 이후 등록된 핸들러 순서대로 진행을 시킨다.

즉, 캡처링 이벤트 핸들러는 버블링 단계보다 먼저 이벤트를 감지할 수 있는 정도의 옵션인 셈이다. 


 

캡처링과 버블링 순서로 이벤트 전파가 진행될 때 상황에 따라 이러한 전파를 차단해야하는 경우가 종종 생긴다. stopPropagation은 이러한 전파를 차단할 수 있는 메소드이다. 

사용하는 방법은 아래와 같다.

<body>
	<div class="one">
		<div class="two">
			<div class="three">
			</div>
		</div>
	</div>
</body>

var divs = document.querySelectorAll('div');

// 이벤트 버블링 예제
divs.forEach(function(div) {
	div.addEventListener('click', logEvent);
});

function logEvent(event) {
	event.stopPropagation();
	console.log(event.currentTarget.className); // three
}

 

이벤트 버블링에 경우에는 클릭한 요소의 이벤트만 발생시키고 최상위 요소까지 이벤트를 전달하는 것을 차단한다.

만약 class가 three인 요소를 클릭했다면 log에는 three가 출력 될 것이다.

<body>
	<div class="one">
		<div class="two">
			<div class="three">
			</div>
		</div>
	</div>
</body>

var divs = document.querySelectorAll('div');

// 이벤트 캡쳐 예제
divs.forEach(function(div) {
	div.addEventListener('click', logEvent, {
		capture: true // default 값은 false입니다.
	});
});

function logEvent(event) {
	event.stopPropagation();
	console.log(event.currentTarget.className); // one
}

 

이벤트 캡처링은 클릭한 요소의 이벤트만 동작을 시키고 하위 요소들로 이벤트를 전달하지 않는다.

만약 class가 three인 요소를 클릭했다면 log에는 one이 출력 될 것이다.


이벤트 전파에 대한 지식을 이해하고 이를 구현하는 리소스를 사용하지 못할 때 stopPropagation 메서드는 괜찮은 해결책이 될 수도 있다. 하지만 stopPropagation 메서드를 상황에 맞지 않게 사용을 할 때 미궁에 수렁속에 빠질 수 있는데, 어떤 상황들이 있는지 한 번 살펴보자. 

<div id="parent" onclick="log('parent')">
  <div id="child">
  </div>
</div>
<script>
  function log(x) { 
    console.log(x); 
  }
  
  const parent = document.getElementById("parent");
  parent.addEventListener('click', function() {
    log("parent in addEventListener");
  });
</script>

 

parent 엘리먼트는 인라인 이벤트 핸들러를 가지면서 script 영역에서 addEventListener에 이벤트 핸들러를 등록했다. 만약 parent 엘리먼트에서 이벤트가 발생한다면 어떤 순서로 실행이 될까? 해당 순서는 다음과 같다. 

 

1. 등록한 인라인 이벤트 실행

2. addEventListener로 등록한 핸들러 실행 

 

결과적으로 addEventListener로 등록한 핸들러에서 stopPropagation을 실행해도 인라인으로 등록된 콜백 이벤트는 멈출 수 없다. 

 

위에 그림을 보면서 다시 한번 복기를 시켜보자.

 

만약 div#parent을 클릭하면 최상위 엘리먼트인 div#app이 실행이 되고, 이때 capture: true인 경우에는 캡처링 단계에서 실행이 될 것이다. 그리고 만약 최상위 엘리먼트에서 capture: true이면서 stopPropagation 메서드를 실행했다면, 이벤트 전파가 이루어지지 않으므로 다음 엘리먼트가 넘겨 받질 않는다. 

 

capture가 true가 아닌 경우 parent 엘리먼트로 전파가 발생하고, 버블링으로 인해 최상위 엘리먼트가 호출이 된다. 만약 parent 엘리먼트에 stopPropagation이 달려 있다면 버블링으로 인한 전파가 일어나지 않는다. 

 

parent 엘리먼트에 인라인 태그로 이벤트 핸들러가 등록이 되어 있다면 인라인 콜백이 실행이 되고 addEventListner로 등록된 핸들러가 실행이 된다. 이때 addEventListner로 등록된 핸들러에 stopPropagation 메서드가 달려 있다고 하더라고 최상위 전파까지는 막을 수 있겠지만 인라인으로 등록된 콜백은 실행이 된다.

 

버블링&캡처링 - CodeSandbox

버블링&캡처링 by wjdrms1919

codesandbox.io


StopImmediatePropagation

메서드 명을 잘 살펴보면 어떤 기능을 하는 함수인지 어느정도 유추를 할 수 있다. stopImmediatePropagation 메서드는 실행된 이벤트를 마지막으로 뒤에 실행 예정이었던 이벤트 핸들러의 실행을 막는다. 

window.addEventListener('click', () => log(1));
window.addEventListener('click', (e) => {
  e.stopImmediatePropagation();
  log(2);
});
window.addEventListener('click', () => log(3));
// print 1 2
[event_listener_list]
index  type    callback      ...
  0    click   (anonymous)
  1    click (anonymous)
  2    click (anonymous)

 

위 코드를 살펴보면 window에 이벤트 핸들러 3개를 등록하였다. 등록된 이벤트는 아래와 같이 이벤트 리스너 목록에 등록된다. 무기명 콜백 함수를 등록하였기 때문에 다른 메모리 주소로 이벤트 등록이 된다. 그리고 호출이 일어날 때는 index 순서대로 실행이 된다.

 

첫 번째 함수에서 log 1이 찍히고 두번째 함수에서 stopImmediatePropagation메서드가 실행되었으므로 log 2가 찍힌후 뒤에 실행 예정이었던 이벤트 핸들러를 무시하게 된다. 

 

stopImmediatePropagation이 stopPropagation과 현저하게 다른점은 이벤트 버블링, 캡처링과 같은 전파를 막기 위함이 아닌, 등록되어 있는 다른 모든 이벤트의 실행을 막는다는 점이다. 

그렇기에 최상위 DOM 엘리먼트에 해당 이벤트 함수를 달아놓았다면 핸들러의 실행이 종료됨과 동시에 등록된 모든 이벤트의 콜백 호출이 무시된다. 


Conclusion

stopImmediatePropagation과 stopPropagation은 동작이 아예 다르기 때문에 상황에 따라 적절히 사용해야 한다. stopPropagation은 캡처링 버블링의 전파를 막고 싶을 때 사용하면 되며, stopImmediatePropagation은 이벤트 전파에 초점을 맞추기보다는 현재 실행 중인 이벤트 이후에 그 어떠한 핸들러 이벤트 실행을 막고 싶을 때 사용하면 유용하다. 

 

또한 인라인으로 작성된 이벤트는 stopPropagation 메서드로 막을 수 없다. ( 단지 console.log를 찍는 일반 실행문의 경우는 막을 수 있다.)


< 참고자료 >

[사이트] #stopImmediatePropagation

 

DOM Standard

 

dom.spec.whatwg.org

[사이트] #event-flow

 

UI Events

 

www.w3.org

stopPropagation vs stopImmediatePropagation end

 
Comments