마우스가 요소 위에 올라갔을 때 처리하려고 mouseover 이벤트를 붙였는데, 자식 요소가 여러 개면 이벤트가 계속 또 발생한다. 이게 뭐 하는 짓인가 싶으면서 결국 mouseenter로 바꿔보니 한 번만 실행된다. 다만 대부분의 개발자들은 mouseover와 mouseenter의 정확한 차이를 모른 채 인터넷에서 가져온 코드를 그냥 쓴다.
이번에는 두 이벤트의 정확한 동작 원리, 버블링이 왜 일어나는지, 그리고 언제 뭘 써야 하는지 완벽하게 정리해서 소개하겠다.
두 이벤트 모두 마우스가 요소 위로 들어올 때 발생한다는 점은 같다. 하지만 결정적인 차이는 이벤트 버블링 여부다.
mouseover는 버블링이 발생한다. 즉, 자식 요소로 마우스가 이동하면 그 자식 요소에서 mouseover 이벤트가 발생하고, 그 이벤트가 부모 요소로 계속 올라간다. 결과적으로 부모에 붙인 mouseover 핸들러가 자식에서도 여러 번 실행된다.
mouseenter는 버블링이 발생하지 않는다. 오직 직접 그 요소 위로 마우스가 들어올 때만 한 번 발생하고, 자식 요소로 이동해도 새로 발생하지 않는다. 자식 요소는 자기 자신의 mouseenter 이벤트만 독립적으로 가진다.
const parent = document.querySelector('.parent');
let count = 0;
parent.addEventListener('mouseover', () => {
count++;
console.log('mouseover 발생:', count);
});
HTML 구조:
<div class="parent">
부모 영역
<div class="child">자식 1</div>
<div class="child">자식 2</div>
</div>
결과: 부모 영역에 마우스를 올리면 1 출력. 그런데 자식 1에 올리면 2, 자식 2에 올리면 3, 자식 1로 다시 올리면 4가 출력된다. 자식 영역으로 이동할 때마다 부모의 mouseover 핸들러가 계속 실행된다.
const parent = document.querySelector('.parent');
let count = 0;
parent.addEventListener('mouseenter', () => {
count++;
console.log('mouseenter 발생:', count);
});
결과: 부모 영역에 마우스를 올리면 1 출력. 자식 1, 자식 2, 다시 자식 1로 이동해도 더 이상 출력되지 않는다. 오직 처음 부모 영역에 마우스가 들어올 때만 한 번 실행된다.
mouseover와 mouseenter의 차이가 정말 체감되는 상황은 드롭다운 메뉴나 호버 효과가 있는 컴포넌트다. mouseover를 쓰면 자식 메뉴로 이동할 때마다 부모의 show/hide 로직이 반복 실행되어서 깜빡거린다.
const menu = document.querySelector('.menu');
const submenu = document.querySelector('.submenu');
menu.addEventListener('mouseover', () => {
submenu.style.display = 'block';
console.log('submenu show');
});
menu.addEventListener('mouseout', () => {
submenu.style.display = 'none';
console.log('submenu hide');
});
<div class="menu">
메뉴
<div class="submenu">
<a href="#">서브 1</a>
<a href="#">서브 2</a>
</div>
</div>
문제: submenu로 마우스가 이동할 때마다 mouseover/mouseout이 연쇄적으로 발생해서 submenu가 보였다가 사라졌다가를 반복한다. 사용자 입장에서는 메뉴가 깜빡거려서 거슬린다.
const menu = document.querySelector('.menu');
const submenu = document.querySelector('.submenu');
menu.addEventListener('mouseenter', () => {
submenu.style.display = 'block';
console.log('submenu show');
});
menu.addEventListener('mouseleave', () => {
submenu.style.display = 'none';
console.log('submenu hide');
});
결과: menu 영역 전체(submenu 포함)에서 마우스가 떠날 때까지 submenu가 유지된다. 사용자가 자연스럽게 메뉴를 탐색할 수 있다.
| 특성 | mouseover / mouseout | mouseenter / mouseleave |
|---|---|---|
| 이벤트 버블링 | O (발생함) | X (발생하지 않음) |
| 자식 요소 진입 시 | 부모 핸들러 재실행 | 부모 핸들러 실행 안 됨 |
| 사용 난이도 | 높음 (버블링 처리 필요) | 낮음 (직관적) |
| 성능 | 낮음 (이벤트 많음) | 높음 (이벤트 적음) |
| 추천 사용 시기 | 버블링이 필요할 때 드묾 | 대부분의 상황 |
동적으로 생성되는 요소들에 mouseenter를 붙이려면 이벤트 위임을 쓸 수 없다. mouseenter는 버블링이 없기 때문이다. 이 경우 부모에 mouseover를 붙이고 stopPropagation()으로 버블링을 막거나, 동적 요소 생성 시점에 직접 mouseenter를 붙여야 한다.
// ✗ 안 됨: 부모에 mouseenter 위임 불가능
container.addEventListener('mouseenter', (e) => {
if (e.target.matches('.dynamic-item')) {
// 동적 요소에 대해 실행 안 됨
}
});
// ✓ 됨: 동적 요소 생성 후 직접 붙이기
function createItem(text) {
const item = document.createElement('div');
item.classList.add('dynamic-item');
item.textContent = text;
item.addEventListener('mouseenter', () => {
item.style.background = 'yellow';
});
container.appendChild(item);
}
CSS :hover를 사용한 경우, JavaScript에서는 mouseenter/mouseleave를 쓰는 게 일관성이 있다. mouseover를 쓰면 CSS와 JavaScript 동작이 미묘하게 다르게 보일 수 있다.
.menu:hover .submenu {
display: block;
}
// CSS :hover와 맞추려면 mouseenter/mouseleave 사용
menu.addEventListener('mouseenter', () => {
submenu.classList.add('active');
});
mouseenter와mouseover의 차이는 이벤트 버블링 여부다. 대부분의 상황에서는 mouseenter/mouseleave를 쓰는 게 코드를 간결하게 하고 성능도 좋다. mouseover/mouseout이 필요한 경우는 정말 드물고, 필요하다면 버블링을 명확히 이해한 후에 써야 한다. 이 글의 코드 예제를 참고해 현재 프로젝트의 호버 이벤트 핸들러를 한 번 점검해보면, 불필요한 이벤트 반복을 줄이고 사용자 경험도 개선할 수 있을 것이다.