우선 버블링
부터 설명해 보자면, 버블링
은 DOM 트리에서 이벤트가 발생했을 때, 그 이벤트가 발생한 특정 요소에서부터 상위 부모 요소로 전파되는 방식입니다. 즉, 가장 구체적인 요소(이벤트가 발생한 요소)에서부터 시작하여 점점 더 넓은 범위(부모, 조상 요소 등)로 이벤트가 전파되는 현상인데요
예를 들어, 클릭 이벤트가 특정 버튼 요소에서 발생하면, 그 이벤트는 버튼의 부모 요소, 그 부모의 부모 요소로 차례대로 전파되는데, 이때 부모 요소 중 이벤트 리스너를 등록 하고 있는 부모 요소는 해당 이벤트 리스너가 실행되는 현상인거죠.
반대로 캡처링
은 버블링과 반대로 부모 요소의 이벤트 발생이 하위 요소까지 전달되는 현상을 의미하는데요. 이러한 이벤트 전파 방식은 리액트뿐만 아니라, DOM의 기본적인 이벤트 전파 메커니즘으로, 모든 브라우저에서 동작하는 자연스러운 현상입니다.
이러한 이벤트 전파는 크게
① 부모에서 타겟으로 내려가면서 등록된 이벤트가 발생되는 캡처 페이즈,
② 타겟 자체에서 등록된 이벤트가 발생되는 타겟 페이즈,
③ 다시 타겟에서 부모로 올라가면서 등록된 이벤트가 발생되는 버블링 페이즈로 구성됩니다.
그럼 카운터 앱 코드를 예시로 버블링과 캡쳐링이 어떻게 일어나는지를 아래의 코드를 잠깐 살펴볼까요?
import { useState } from "react"; function CounterApp() { const [count, setCount] = useState(0); // 캡처링 단계에서 실행될 함수 const handleParentClickCapture = () => { console.log("부모 캡처링 단계 발생 (count: " + count + ")"); }; // 버블링 단계에서 실행될 함수 const handleParentClick = () => { console.log("부모 버블링 단계 발생 (count: " + count + ")"); }; // 증가 버튼 클릭 핸들러 const handleIncrement = () => { setCount((prevCount) => prevCount + 1); console.log("증가 버튼 클릭: " + count); }; // 감소 버튼 클릭 핸들러 const handleDecrement = () => { setCount((prevCount) => prevCount - 1); console.log("감소 버튼 클릭: " + count); }; return ( <div onClickCapture={handleParentClickCapture} // 캡처링 단계에서 발생 onClick={handleParentClick} // 버블링 단계에서 발생 style={{ padding: "50px", border: "2px solid black", textAlign: "center", }} <h1>Counter: {count}</h1> <div> <button onClick={handleIncrement} style={{ margin: "10px", padding: "10px" }} Increment </button> <button onClick={handleDecrement} style={{ margin: "10px", padding: "10px" }} Decrement </button> </div> </div> ); } export default CounterApp;
increment
버튼을 누르면 최상위 부모 요소인 <div>
태그에 등록된 onClickCapture
속성(태그의 고유 제공 속성)에 등록된 함수인 handleParentClickCapture
가 발동handleIncrement
실행<div>
태그에 등록된 onClick
속성(버블링 현상이 일어날 때 발동되는 속성이기도 함)에 등록된 함수인 handleParentClick
가 발동이렇듯 버블링과 캡처링으로 인해 발생되는 전파 현상, 그러니까 개발자의 컨트롤에서 벗어나는 이벤트의 발생 현상을 막기 위해서는 타겟이 되는 요소에 stopPropagation
함수를 걸어주면 되는데요. 아래의 코드 예시를 살펴볼까요?
import { useState } from "react"; function CounterApp() { const [count, setCount] = useState(0); // 캡처링 단계에서 실행될 함수 const handleParentClickCapture = () => { console.log("부모 캡처링 단계 발생 (count: " + count + ")"); }; // 버블링 단계에서 실행될 함수 const handleParentClick = () => { console.log("부모 버블링 단계 발생 (count: " + count + ")"); }; // 증가 버튼 클릭 핸들러 (이벤트 전파 중단) const handleIncrement = (event) => { event.stopPropagation(); // 이벤트 전파 중단 (버블링 차단) setCount((prevCount) => prevCount + 1); console.log("증가 버튼 클릭: " + count); }; // 감소 버튼 클릭 핸들러 (이벤트 전파 중단) const handleDecrement = (event) => { event.stopPropagation(); // 이벤트 전파 중단 (버블링 차단) setCount((prevCount) => prevCount - 1); console.log("감소 버튼 클릭: " + count); }; return ( <div onClickCapture={handleParentClickCapture} // 캡처링 단계에서 발생 onClick={handleParentClick} // 버블링 단계에서 발생 style={{ padding: "50px", border: "2px solid black", textAlign: "center", }} <h1>Counter: {count}</h1> <div> <button onClick={handleIncrement} style={{ margin: "10px", padding: "10px" }} Increment </button> <button onClick={handleDecrement} style={{ margin: "10px", padding: "10px" }} Decrement </button> </div> </div> ); } export default CounterApp;
위 코드에서는 이벤트가 최초로 발생 되는 타겟 (DOM)의 이벤트에 stopPropagation
함수가 먼저 호출이 되는 것을 확인할 수 있습니다. 이렇게 되면 최초 이벤트 발생 타겟을 중심으로 버블링이 차단되기 때문에 부모로부터 자식에게 전파되는 캡처링과 타겟에서만 발생된 이벤트만 발생할 수 있습니다.
그러나 방금 말씀드린대로 위 코드에서 캡처링 속성은 설정이 되어있기 때문에(onClickCapture
) 캡처링 전파는 발생이 되는데요. 현업에서는 굳이 onClickCapture
속성을 정의하지 않기 때문에 자주 사용되는 onClick
버튼에 등록된 이벤트 전파 발생 (버블링)에 대한 방지 방법만 알고 계셔도 충분합니다.