회사에서 프로젝트를 진행하는 도중 예상하지 못한 상황에 마주하였다.
모달 닫기 창을 click하면 닫힌 이후 mouseEnter 이벤트가 발생하였다.
내가 진행하는 프로젝트는 스택 형식으로 팝업이 쌓이면서 이동이 이루어지는데,
모달 팝업을 마우스로 끄고 다음 페이지로 넘어갔을 때 LNB가 mouseEnter 이벤트를 받고 확장이 되는 것이었다.
그리고 마우스를 약간 움직이면 순간적으로 확장되었다가 사라지는데,
깜빡 거리는 느낌처럼 보여서 좋은 경험을 받지 않았다.
의심되는 상황은 event.stopPropagation이었고, 이를 먼저 적용해 보기로 하였다.
기존 코드는 이렇다
export default function LNB({ handleCloseView }) {
return (
<nav className='LNB'>
<button
className='btn-back'
onClick={handleCloseView}
/>
</nav>
);
}
LNB를 클릭하면 팝업이 닫히는데,
그 아래에 있는 페이지 LNB에서 mouseEnter가 일어난다.
이벤트를 다루기 위해서 사용되는 2가지 이벤트가 있는데, event.stopPropagation과 event.preventDefalut이다.
event.stopPropagation()
이 메서드는 이벤트가 DOM 트리를 통해 전파(bubbling)되는 것을 중지합니다.
button.addEventListener("click", function(event) { alert("Button clicked!"); // 이 코드가 실행됩니다. event.stopPropagation(); // 여기서 이벤트 전파를 중지 }); document.body.addEventListener("click", function() { alert("Body clicked!"); // 이 코드는 실행되지 않습니다. });
event.preventDefault()
이 메서드는 브라우저의 기본 동작을 중지합니다.
Copy code const link = document.querySelector("a"); link.addEventListener("click", function(event) { alert("Link clicked!"); // 이 코드가 실행됩니다. event.preventDefault(); // 여기서 기본 동작을 중지 });
위 내용을 적용해 보아도 이벤트가 연속(click-> mouseEnter)해서 발생하였다.
stopPropagation은 부모로 이벤트가 전파되는데, 뒤에 있는 팝업은 부모가 아니기 때문에, 영향이 없다고 생각하였고,
preventDefalut는 브라우저의 기본 동작을 중지하는 역할이어서 이벤트와는 무관하였다.
이벤트를 발생 시킬 때, 특정 대상을 지정해준다면, 이후 DOM요소는 특정 대상이 아니기때문에, 괜찮지 않을까? 생각하였다.
그렇게 찾다 보니 event.target과 event.currentTarget을 알게 되었다.
event.target
event.target은 실제로 이벤트가 발생한 DOM 요소를 가리킵니다. 예를 들어 버튼 내부에 이미지가 있고 사용자가 그 이미지를 클릭했다면, event.target은 그 이미지 요소가 됩니다.
event.currentTarget
e.currentTarget은 이벤트 리스너가 실제로 부착된 DOM 요소를 가리킵니다. 즉, 위의 예에서 버튼에 이벤트 리스너가 부착되어 있다면, e.currentTarget은 버튼 요소가 됩니다.
<button onclick={handleClick}>
<img />
</button>
위에서 이미지를 클릭하면
function handleClick(e) {
console.log(e.target === e.currentTarget); // 일반적으로 false
console.log(e.target); // 클릭된 실제 요소 (예: <img>)
console.log(e.currentTarget); // 이벤트가 부착된 요소 (<button>)
}
위처럼 될 것이라고 생각하여서 적용해 보았다.
const handleOnClick = (e) => {
if (e.currentTarget === e.target) {
e.preventDefault();
e.stopPropagation()
handleCloseView();
}
}
return (
<nav className='LNB'>
<button
className='btn-back'
onClick={handleOnClick}
/>
</nav>
);
결과는 실패하였다.
이벤트가 발생이 되면 이어서 뒤 화면에서도 이어졌다.
여러 디버깅 결과 onClick 이후에 모달이 사라져도
뒤에 있는 DOM 요소에 마우스가 들어오는 것으로 인식하여서
문제가 생긴다고 판단하였다.
그러면 마우스 이벤트가 끝난 뒤에 넘어간다면 어떨지 라는 생각이 들었다.
이벤트가 먼저 발생하면 webAPI 이를 감지하고 태스크 큐에 넣어주고
콜스택이 비어있을 때 이벤트 루프가 이를 넣어주어서 실행한다.
그러면 setTimeout은 매크로 테스크 큐로 들어가서 실행되기 때문에,
이벤트가 다끝나고 나서 실행이 될 것으로 예상하였다.
예를 들면
console.log('Start'); // 매크로태스크
setTimeout(() => {
console.log('Timeout'); // 매크로태스크
}, 0);
Promise.resolve().then(() => {
console.log('Promise'); // 마이크로태스크
});
console.log('End'); // 매크로태스크
Start, End, Promise, Timeout순으로 실행된다.
이렇게 되는 이유는 동기적으로 콘솔로그가 실행되고 이후 비동기 작업이 이벤트 루프에 의해 처리한다.
이후 이벤트 루프는 마이크로태스크(Promise)가 먼저 처리되고, 그 후에 다음 매크로태스크(Timeout)를 처리한다.
위 개념을 통해 setTimeout을 활용하였다.
export default function LNB({ handleCloseView }) {
const handleOnClick = () => {
setTimeout(() => { handleCloseView(); })
};
return (
<nav className='LNB'>
<button
className='btn-back'
onClick={handleOnClick}
/>
</nav>
);
}
위 코드를 통해 해결하였다.
이번 이슈를 통해 이벤트가 어떻게 처리되는지 깊게 생각해 볼 수 있었다.
event.stopPropagation이 어떻게 요소를 타고 올라가는지나,
event.currentTarget에 대한 개념 등을 배울 수 있었다.
그리고 자바스크립트가 싱글 스레드 방식으로 비동기를 어떻게 처리하는지에 관해 공부하였었는데 이 cs지식을 활용하여서 문제를 해결할 수 있다는 점이 인상 깊었다.
부트캠프 시절에는 디버깅을 통해 유추만 하였는데 기본 골격이 어떻게 돌아가는지 알아서 같은 디버깅을 해도 더 깊은 이해를 할 수 있었다.