통합 쇼핑 플랫폼 - 12st Troubleshooting 🚀 (이벤트 버블링)
구현하고자 했던 기능은 다음과 같다.
- 쇼핑 카트 버튼을 클릭하면 장바구니 모달이 나타난다.
- 해당 모달을 사라지게 하는 방법은 '닫기'와 모달의 바깥 영역을 클릭하는 방법이 있다.
간단한 기능이지만, 기존과는 다른 모달 구현 방법으로 인해 어려움을 겪었다. 이번 게시글에서는 모달 구현을 위한 과정을 기록하고자 한다.
🤔 이렇게 하면 되지 않을까..?
// cartModal.tsx
useEffect(() => {
window.addEventListener("click", onhideCartModal);
return () => {
window.removeEventListener("click", onhideCartModal);
};
}, []);
CartModal 컴포넌트가 마운트 되었을 때 window의 click 이벤트에 cartModal을 언마운트 시키는 함수를 연결시켜 주었다.
결과는 CartModal이 마운트 되자 말자 바로 언마운트된다. 이는 쇼핑 카트 버튼도 window 영역에 해당하기 때문이다.
의도한 바는 CartModal이 마운트 되고 CartModal 바깥 영역을 클릭했을 때 CartModal이 언마운트되는 것이었다. 하지만 쇼핑 카트를 클릭한 것 또한 window 영역을 클릭한 것이기 때문에 연결해 준 onhideCartModal 함수가 실행되어 언마운트 되었다.
만약 CartModal이 정상적으로 마운트 된다 하더라도 CartModal 영역 또한 window 영역에 해당하기 때문에 '닫기' 버튼을 클릭하지 않고 모달의 아무 영역을 클릭해도 onhideCartModal 함수가 실행되었다.
이점을 해결하기 위해서는 이벤트 버블링에 대해 알 필요가 있었다.
🤔 이벤트 버블링..?
// html
<body>
<div class="parent">
<div class="child">
<a href="javascript:void(0)">aaa</a>
</div>
</div>
</body>
class가 parent div 내부에 class가 child인 div가 있으며, class가 child인 div 내부에 a 태그가 있다.
const parentEl = document.querySelector(".parent");
const childEl = document.querySelector(".child");
const anchorEl = document.querySelector("a");
window.addEventListener("click", (event) => {
console.log("window 클릭!");
});
document.body.addEventListener("click", (event) => {
console.log("body 클릭!");
});
parentEl.addEventListener("click", (event) => {
console.log("parent 클릭!");
});
childEl.addEventListener("click", (event) => {
console.log("child 클릭!");
});
anchorEl.addEventListener("click", (event) => {
console.log("anchor 클릭!");
});
window, body, class가 parent, class가 child, a태그에 각각의 이벤트를 추가해 주었다.
이때 a 태그를 클릭하게 되면 위와 같은 콘솔이 출력이 된다.
위와 같은 결과가 나온 이유는 a태그의 영역이 child의 영역이고, child의 영역이 parent의 영역, parent의 영역이 body의 영역, body의 영역이 window의 영역이기 때문이다. 즉 a 태그를 클릭한 것이 window를 클릭한 것과 같다.
이때 콘솔 출력의 순서는 가장 하위에 있는 a태그부터 시작해서 가장 상위에 있는 window로 끝이 난다.
이렇게 작은 범위에서 시작해서 점점 넓은 범위로 이벤트가 올라가는 현상을 이벤트 버블링이라고 하며 후손 요소로부터 상위 요소로 이벤트가 확산되는 것을 이벤트 전파라고도 한다.
parentEl.addEventListener("click", (event) => {
event.stopPropagation(); // 이벤트 전파(버블링) 정지
console.log("parent 클릭!");
});
parent에서 이벤트 전파를 정지시켰기 때문에 클릭 이벤트가 상위 요소인 body와 window로 확산되지 않고 끝이 난다. 즉 콘솔이 parent 까지만 출력된다.
event 객체의 stopPropagtion 메서드를 사용하면 이벤트 전파(버블링)를 정지할 수 있게 된다.
( propagation : 전파 )
🚀 프로젝트 적용
구현하고자 하는 기능에서는 두 번의 이벤트 전파 정지가 필요하다.
우선 CartModal 요소에서 상위 요소로 클릭 이벤트가 전파되는 것을 정지해줘야 한다.
다음으로 쇼핑카트 버튼 요소에서 상위 요소로 클릭 이벤트가 전파되는 것을 정지해줘야 한다.
// CartModal.tsx
const onClickCartModal = (e: MouseEvent) => {
// 이벤트 전파 정지
e.stopPropagation();
};
useEffect(() => {
window.addEventListener("click", onhideCartModal);
return () => {
window.removeEventListener("click", onhideCartModal);
};
}, []);
// CartModalView.tsx
return (
<CartModalContainer
isShowCartModal={isShowCartModal}
onClick={onClickCartModal}
>
CartModal에서 클릭 이벤트 전파 정지를 위해 stopPropagation 메서드를 사용해 주었으며, 해당 함수를 CartModal의 가장 상위 요소의 이벤트로 연결시켜 주었다.
//HeaderCart.tsx
const onShowCartModal = (e: MouseEvent) => {
e.stopPropagation();
setIsShowCartModal(true);
};
쇼핑카트 버튼에서 클릭 이벤트 전파 정지를 위해 동일하게 stopPropagation 메서드를 사용해 주었다.
이벤트 전파 정지를 통해 구현하고 했던 기능을 구현할 수 있었다.