Web Element Event
웹개발을 하며 자바스크립트를 사용해 여러 DOM 요소의 이벤트를 처리함에 있어 stopPropagation
, preventDefault
에 대해 알아보자.
웹 이벤트의 순서
이벤트는 다음과 같은 순서로 진행된다.
- Capture Phase
- Target Phase
- Bubbling Phase
-
Capture phase (캡처 단계): 이벤트는 window 객체부터 시작해서 이벤트 발생 목표(Target)로 곧장 내려가는 단계이다.
-
Target phase (타겟 단계): 이벤트가 실제로 발생한 목표(Target) 요소에 도달하는 단계이다. 타겟 단계에서는 해당 요소에서 이벤트 핸들러가 처리된다.
-
Bubbling phase (버블링 단계): 이벤트가 Target 요소에서 부모 요소를 거슬러 다시올라가는 단계이다.
우리가 이벤트 핸들러를 처리하는 부분들은 대개 Target phase나 Bubbling Phase이다.
예를 들어, 어떤 <button>
을 클릭했을 때 button
element 자체에 이벤트 핸들러를 달아두었다고 하자.
그럼 그 버튼을 클릭했을 때 이벤트 핸들러가 호출되는 단계는 Target phase이다.
<body>
<div id="parent">
<button id="btn">Button</button>
</div>
<script>
document.querySelector('#btn').addEventListener('click', (e) => {
console.log('btn');
});
</script>
</body>
여기서 버튼을 누르면 btn
이 보일 것이다.
그런데 다음과 같은 경우엔 버튼을 누르면 parent
가 보일까?
<body>
<div id="parent">
<button id="btn">Button</button>
</div>
<script>
document.querySelector('#btn').addEventListener('click', (e) => {
console.log('btn');
});
document.querySelector('#parent').addEventListener('click', (e) => {
console.log('parent');
});
</script>
</body>
다른 플랫폼들에선 gesture가 자식에게서 소비되었으니 부모가 전달받지 못하지만 웹에서는 이또한 호출된다.
btn
parent
웹에서의 이벤트는 항상 (1) Capturing - (2) Target - (3) Bubbling 을 통해 전파되기 때문이다.
이런 전파를 막을 수 있는 방법은 stopPropagation
인데, 아래서 기술된다.
Default는 bubbling phase에서 캡쳐되는 것이다.
위 예시에서 순서가 중요하다. 왜 btn
이 호출되고 parent
가 출력되었을까?
그것을 알아보기 위해 각 이벤트의 eventPhase
속성을 살펴보자.
이는 이벤트 리스너로 들어오는 이벤트 객체에서 참조할 수 있고 다음과 같은 열거값을 갖는다.
- 0: 이벤트 전파가 아직 발생하지 않았거나,
event.stopPropagation()
메서드에 의해 전파가 중단되었을 경우에 0 - 1: Capturing Phase
- 2: Target Phase
- 3: Bubbling Phase
<body>
<div id="parent">
<button id="btn">Button</button>
</div>
<script>
document.querySelector('#btn').addEventListener('click', (e) => {
console.log('btn', e.eventPhase);
});
document.querySelector('#parent').addEventListener('click', (e) => {
console.log('parent', e.eventPhase);
});
</script>
</body>
btn 2
parent 3
버튼은 Target Phase, 부모 div는 Bubbling Phase에서 이벤트를 받게된 것을 알 수 있다.
Target 그 자체가 아닐 경우 기본적으로 Bubbling Phase에서 이벤트를 받게 되기 때문에 부모의 출력문이 더 늦게 나온 것이다.
Capturing 단계에서 이벤트를 받는법
addEventListener
의 세 번째 인자로 true
나 옵션 객체에 capture: true
를 전달하면 된다.
<body>
<div id="parent">
<button id="btn">Button</button>
</div>
<script>
document.querySelector('#btn').addEventListener(
'click',
(e) => {
console.log('btn', e.eventPhase);
},
true,
);
document.querySelector('#parent').addEventListener(
'click',
(e) => {
console.log('parent', e.eventPhase);
},
true,
);
</script>
</body>
parent 1
btn 2
이제 부모의 출력문이 더 먼저 나온다.
Capturing이 Target Phase보다 더 일찍 일어나기 때문이다.
버튼은 이벤트의 주체이기 때문에 eventPhase
가 여전히 Target Phase를 가리키고 있다.
React에서 이벤트의 캡슐화
일부러 본문을 바닐라 JS로 작성했지만 React에서 이것을 시도해보면 조금 다르다.
<div onClick={(e) => console.log('parent', e.eventPhase)}>
<button onClick={(e) => console.log('btn', e.eventPhase)}>{'Button'}</button>
</div>
btn 3
parent 3
Target Phase라는게 없어진걸 볼 수 있다. 항상 Bubbling으로만 처리된다.
이것은 React가 이벤트를 처리하는 방식이 DOM과 완전히 동일하지않고 React만의 shortcut Prop들이나 캡슐화를 해두었기 때문이다. React Docs
또한 여러 Shortcut의 존재와 더불어 Capturing 이벤트는 onClick
이 아닌 onClickCapture
와 같은 것으로 쉽게 구현할 수 있다.
target vs currentTarget
이벤트 핸들러에서 이벤트 객체의 target
과 currentTarget
은 조금 다른의미를 가진다.
target
은 이벤트가 발생한 주체이고 currentTarget
은 이벤트 핸들러가 등록된 주체이다.
이 예시를 보자.
<body>
<div id="parent">
<button id="btn">Button</button>
</div>
<script>
document.querySelector('#btn').addEventListener('click', (e) => {
console.log('btn', e.target.tagName, e.currentTarget.tagName);
});
document.querySelector('#parent').addEventListener('click', (e) => {
console.log('parent', e.target.tagName, e.currentTarget.tagName);
});
</script>
</body>
버튼을 클릭하면 다음과 같이 된다.
btn BUTTON BUTTON
parent BUTTON DIV
버튼이 클릭되었으므로 e.target
은 이벤트의 주체인 button
이다.
currentTarget
은 이벤트 핸들러가 등록된 주체를 나타낸다고 했으므로 parent
의 이벤트 핸들러에서 currentTarget
은 div
를 가리킴이 옳게된다.
stopPropagation
우린 기본적인 개념을 모두 잡았으므로 이걸 쉽게 이해할 수 있다.
e.stopPropagation
의 함수 이름에서도 볼 수 있듯이 이건 이벤트의 전파를 중지시키는 함수이다.
현재 캡쳐링 단계라면 트리의 아래로 가는 전파를, 버블링이라면 트리의 위로 올라가는 전파를 막는다.
어렵게 생각하지말고 그냥 이 이벤트를 다른 이벤트 핸들러에서 더 못쓰게 만든다고 생각하면 쉽다.
<body>
<div id="parent">
<button id="btn">Button</button>
</div>
<script>
document.querySelector('#btn').addEventListener('click', (e) => {
console.log('btn');
e.stopPropagation();
});
document.querySelector('#parent').addEventListener('click', (e) => {
console.log('parent');
});
</script>
</body>
이제 버튼을 눌러도 parent
가 출력되지 않는다.
preventDefault
preventDefault
는 브라우저에서 이 이벤트가 수행됨으로 인해 발생되는 기본 동작들을 중지시킨다.
preventDefault
도 event 객체에서 e.preventDefault()
처럼 호출할 수 있고 stopPropagation
과 혼동하지 않는것이 중요하다.
예를 들어 a
태그는 누르면 링크로 이동시키는데 이걸 방지하고 싶다고 하자.
<body>
<a id="link" href="https://www.mjstudio.net" target="_blank" rel="noopener noreferrer">Link</a>
<script>
document.querySelector('#link').addEventListener('click', (e) => {
console.log('link');
e.preventDefault(); // <- here
});
</script>
</body>
원래대로라면 콘솔에 link
도 출력되고 페이지도 이동되어야 했지만, 페이지가 이동되지 않게 된다.
Comments