React의 이벤트 전파
Introduction
React 이벤트 핸들러 방식과 vanilla Javascript의 이벤트 핸들러 방식을 혼용해서 사용할 때 이벤트 전파가 의도한대로 동작하지 않을 수 있다.
예를 들면 아래와 같이 button1
, button2
구조에서 button2
를 클릭했을 때 button1의
이벤트 핸들러도 실행되는 문제가 발생한다.
const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});
const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};
return (
<button id='button1'>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);
// When button2 is clicked
button2 clicked
button1 clicked
Solution
React에서 이벤트 전파는 vanilla Javascript에서 사용하는 이벤트 전파와 다르다. React에서 이벤트 전파를 delegation 방식으로 처리하기 때문이다. (comment)
React에서는 (React 17 이후로) 이벤트 리스너를 rootDOM에 등록해서 사용한다. 따라서 React끼리의 event listener는 생각한 대로 DOM 구조에 따라 이벤트가 전파가 전달되지만 vanilla Javascript의 이벤트 전파는 생각대로 동작하지 않는다.
가능하다면 두가지 이벤트를 섞어서 쓰지 않는 방향이 좋겠지만, 3rd party library를 사용하거나 제어하지 못하는 부분의 이벤트는 제어하기 힘들기 때문에 동작 방식에 맞춰 이벤트 전파를 막아야한다.
// Unify event listener to vanilla Javascript
const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});
const button2El = document.getElementById('button2');
button2El.addEventListener('click', (e) => {
e.stopPropagation();
console.log('button2 clicked');
});
return (
<button id='button1'>
<button id='button2'>Click me</button>
</button>
);
// Or
// Unify event listener to React
const handleClick1 = (e) => {
console.log('button1 clicked');
};
const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};
return (
<button id='button1' onClick={handleClick1}>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);
Step by Step
이 내용은 Javascript의 Event Bubbling & Capturing을 이해하고 있다는 가정하에 설명한다.
만약 이해가 안된다면 MDN을 참고하자.
React의 이벤트 전파를 이해하기 위해 아래와 같은 예제를 살펴보자.
Figure 1,2
는 각각의 방식으로 이벤트 전파를 설정하였을 때 UI를 보여준다. #1,2,3
아무 곳이나 클릭해보자.
#1
#2
#3
#1
#2
#3
Figure 1
에서는 Vanilla Javascript의 이벤트만 설정하였기 때문에 붉은 색으로 이벤트 전파가 일어난다.
마찬가지로 Figure 2
에서는 React의 이벤트만 설정하였기 때문에 파란 색으로 이벤트 전파가 일어난다.
그렇다면 두 종류의 이벤트를 모두 설정한다면 어떻게 될까? Figure 3
을 눌러보자.
#1
#2
#3
Figure 3
에서는 두 종류의 이벤트를 모두 설정해두어서 이벤트가 모두 발생하고, 색은 보라색으로 표시된다.
이벤트의 발생 순서는 다음과 같다.
Order | Target | Vanilla | React | Type |
---|---|---|---|---|
1 | #1 | ✅ | ✅ | Capture |
2 | #2 | ✅ | ✅ | Capture |
3 | #3 | ✅ | ✅ | Capture |
4 | #3 | ✅ | ✅ | Bubble |
5 | #2 | ✅ | ✅ | Bubble |
6 | #1 | ✅ | ✅ | Bubble |
이벤트 전파를 막지 않았기 때문에 이벤트가 모두 발생하고, 이벤트 전파가 일어난다.
#1 -> #2 -> #3 순으로 capturing이 발생하고 #3 -> #2 -> #1 순으로 bubbling이 발생한다.
그렇다면, 이벤트 전파를 막으면 어떻게 될까? Figure 4,5
를 눌러보자.
전파를 막은 방식은 모두 동일하게 e.stopPropagation()
으로 사용하였다.
// React
<button onClick={(e) => e.stopPropagation()}></button>
// Vanilla
button.addEventListener('click', (e) => {
e.stopPropagation();
});
Figure 4
는 #2
의 React Event bubbling을 막은 경우이고, Figure 5
는 #2
의 Vanilla Event bubbling을 막은 경우이다.
#1
#2
#3
#1
#2
#3
두 예제의 동작은 다르게 나타난다. 결과를 정리해보자.
Figure 4. #2 Stop Bubbling by React
Order | Target | Vanilla | React | Type |
---|---|---|---|---|
1 | #1 | ✅ | ✅ | Capture |
2 | #2 | ✅ | ✅ | Capture |
3 | #3 | ✅ | ✅ | Capture |
4 | #3 | ✅ | ✅ | Bubble |
5 | #2 | ✅ | ✅ | Bubble |
6 | #1 | ✅ | ❌ | Bubble |
Figure 5. #2 Stop Bubbling by Vanilla
Order | Target | Vanilla | React | Type |
---|---|---|---|---|
1 | #1 | ✅ | ✅ | Capture |
2 | #2 | ✅ | ✅ | Capture |
3 | #3 | ✅ | ✅ | Capture |
4 | #3 | ✅ | ❌ | Bubble |
5 | #2 | ✅ | ❌ | Bubble |
6 | #1 | ❌ | ❌ | Bubble |
결과가 복잡 할 수 있는데, 이벤트 전파를 막은 방식과 동일한 이벤트의 결과를 살펴보자.
Figure 4
은 React 방식으로 전파를 막았으므로 Figure 4
의 React 방식은 #1
의 이벤트만 발생하지 않았고,
Figure 5
는 Vanilla Javascript 방식으로 막았으므로 Figure 5
의 Vanilla Javascript 방식도 #1
의 이벤트만 발생하지 않았다.
여기까지는 우리가 익히 아는 이벤트 전파 방식이다. 그러면 다른 방식의 이벤트 전파 결과를 살펴보자.
Figure 4
의 Vanilla 이벤트는 #2
의 React bubble 전파를 막았음에도 이벤트가 발생하였다.
이와 다르게 Figure 5
의 React 이벤트는 #2
의 Vanilla bubble 전파를 막았는데, #1, #2, #3 모두 이벤트가 발생하지 않았다.
여기서 중요한 사실은 React의 이벤트 핸들러 방식은 node에 직접 이벤트를 등록하지 않고, rootDOM
에 등록하여 사용한다는 것이다. (docs)
이 사실을 기반하여 Figure 4
와 Figure 5