Javascript 이벤트 preventDefault, stopPropagation 정리

https://medium.com/p/eac7d4343f99 에서 옮겨온 게시물입니다.

  • 작성시기: 2021-11-23

Change Logs

  • 2022/04/02 09:39 : default event 에 관한 잘못된 개념 수정

자바스크립트 이벤트를 사용하면서 Bubbling, Capturing, preventDefault, stopPropagation 의 개념을 기계적으로만 이해하고 로직의 구현만을 위해 구체적인 동작방식을 알고있지 않았다. 구체적인 알고리즘과 개념이 머릿속에 정립되지 않은 것이 개발자로서 계속 신경쓰여, 호기심이 생긴 김에 예제를 돌려가며 한없이 구체적이고 정확한 개념을 정립하는 시간을 갖고자 한다. 이 예제를 이해하면, preventDefaultstopPropagation 을 제대로 사용할 수 있을것이라 자신한다.

설명에 앞서, Bubbling 과 Capturing 의 개념은 다른분의 포스팅에 잘 정리돼있을 뿐더러, 해당 개념을 이미 알고 있다고 가정하고 진행할 것임을 밝힌다.

웹에서 발생하는 이벤트의 종류는 크게 2가지로 분류할 수 있다.

  • 디폴트 이벤트 : a 태그 클릭 시 hyper reference 로 이동하는 동작, form이 submit 되는 동작 등 기본적으로 태그가 수행하는 이벤트
  • 이벤트 리스너 : addEventListener로 이벤트에 후킹하는 형태로 추가한 콜백함수

크게 이 두가지가 Capturing Phase, Bubbling Phase 의 흐름에 따라 동작하게 된다. 이벤트 리스너는 Capturing Phase에 동작할지, Bubbling Phase에 동작할지 addEventListener 함수의 3번째 인자에 전달하여 정할 수가 있는데 보통은 디폴트처리되어 Bubbling Phase 에 동작하는 편이다. 디풀트 이벤트는 모든 Phase 가 끝나고 나서야 가장 자식인 Element 먼저 시작하고 부모의 것을 실행하게 된다. 이러한 큰 흐름에서, 앞서 언급한 preventDefault, stopPropagation의 개념을 접목시켜 이 두가지가 정확히 어떤 동작을 하는 것인지 정리할 것이다.

stopPropagation 의 정확한 동작

간단히 결론을 앞세워 요약하자면,

  • Phase를 통한 전파 중단.
  • 디폴트이벤트는 그대로 유지

하는 동작을 취한다. 아래의 예시를 살펴보자.

<div class="box1">
  box1
  <div class="box2">
    box2
    <div class="box3">
      box3
      <div class="box4">
        box4
        <div class="box5">
          box5
        </div>
      </div>
    </div>
  </div>
</div>
<script>
  const box1 = document.querySelector('.box1');
  const box2 = document.querySelector('.box2');
  const box3 = document.querySelector('.box3');
  const box4 = document.querySelector('.box4');
  const box5 = document.querySelector('.box5');
  box1.addEventListener('click', e => { console.log('box1 clicked') })
  box2.addEventListener('click', e => { console.log('box2 clicked') })
  box3.addEventListener('click', e => { e.stopPropagation(); console.log('box3 clicked') }, true)
  box4.addEventListener('click', e => { console.log('box4 clicked') })
  box5.addEventListener('click', e => { console.log('box5 clicked') })
 </script>

현재 .box1>.box2>.box3>.box4>.box5 와 같이 요소가 배치된 상태이고, 각각의 클릭 이벤트리스너가 설정된 상태이다. 그리고 .box3 을 제외한 요소는 모두 Bubbling Phase 를 따르고 있고 .box3 만 Capturing Phase 를 따르고 있다.

일단 .box3 의 이벤트리스너 속 e.stopPropagation() 코드를 제거한다면, .box5 클릭 시 console 출력 결과는 box3 clicked, box5 clicked, box4 clicked, box2 clicked, box1 clicked 를 따를것이다. 왜냐하면 브라우저에서 Capturing Phase 에 의해 .box3 의 이벤트 리스너가 동작할 것이고, 그 후 Capturing Phase에 후킹된 이벤트리스너가 없다가 Bubbling Phase 가 진행되기 때문이다. e.stopPropagation()이 삽입되면, Capturing Phase 에서부터 이벤트 전파가 막혀 더이상 나머지의 이벤트리스너가 실행되지 않는 효과가 발생한다.

하지만, 디폴트이벤트는 계속 동작한다. 왜냐하면 Phase 의 진행이 일찍 끝났을 뿐, Phase 를 진행하면서 e.preventDefault() 를 만나지 않았기 때문이다!!

preventDefault 의 정확한 동작

간단히 결론을 앞세워 요약하자면,

  • Phase 내의 모든 요소의 해당 이벤트의 디폴트이벤트를 방지
  • propagation은 유지된다.

는 특성을 지닌다. 이번에도 예제를 통하여 확인해보자.

<form action="https://naver.com" method="post">
  <button type="submit">
    <a href="https://test.com">child</a>
  </button>
</form>
<script>
  const form = document.querySelector('form');
  const button = document.querySelector('button');
  const link = document.querySelector('a');
  
  form.addEventListener('click', (e) => { console.log('form clicked'); });
  button.addEventListener('click', (e) => { e.preventDefault(); console.log('submit button clicked'); });
  link.addEventListener('click', (e) => { console.log('link clicked'); });
 </script>

폼 안의 submit button이 있고 submit button의 자식에 a 태그가 있다. submit button 안에 a 태그가 있는건 분명 웹표준에는 어긋나다(button 안에는 a태그와 같은 대화형컨텐츠를 제외한 구문컨텐츠만이 올 수 있기때문에. see: https://developer.mozilla.org/ko/docs/Web/HTML/Element/button) 하지만, 이벤트 알고리즘을 확인하는 용도의 실습을 위해서 웹표준을 살짝 무시해보자.

a 요소를 클릭해보자. 결론부터 말하자면 test.com 으로 이동하는 디폴트이벤트가 발생하지 않는데, Phase 가 진행하면서 e.preventDefault()를 만나 Phase 내의 모든 요소의 디폴트이벤트가 방지되었던 것이다.

그러나 preventDefault 를 했다고 해서 이벤트가 전파되는 Phase 는 멈추지 않는다. 콘솔에 link clicked, submit button clicked, form clicked 가 정상적으로 나오는 것을 확인할 수 있다. 이로써 나는 이벤트리스너와 디폴트이벤트는 이러한 방식으로 따로간다고 결론지었다.

Phase 진행 후 Default Event 가 제일 자식부터 실행된다. Phase 중에 stopPropagation() 을 만났다면 Phase 가 끝났을 뿐 Default Event가 막히는 것은 아니다. 반면 preventDefault 를 만났다면 Phase 는 계속 그대로 진행하고 Phase 종료 후 Default Event 는 실행되지 않는다.

좋은 웹페이지 즐겨찾기