마우스가 요소 위에 올라갔을 때 처리하려고 mouseover 이벤트를 붙였는데, 자식 요소가 여러 개면 이벤트가 계속 또 발생한다. 이게 뭐 하는 짓인가 싶으면서 결국 mouseenter로 바꿔보니 한 번만 실행된다. 다만 대부분의 개발자들은 mouseover와 mouseenter의 정확한 차이를 모른 채 인터넷에서 가져온 코드를 그냥 쓴다.

이번에는 두 이벤트의 정확한 동작 원리, 버블링이 왜 일어나는지, 그리고 언제 뭘 써야 하는지 완벽하게 정리해서 소개하겠다.

 

mouseover와 mouseenter의 근본 차이: 버블링 유무

두 이벤트 모두 마우스가 요소 위로 들어올 때 발생한다는 점은 같다. 하지만 결정적인 차이는 이벤트 버블링 여부다.

mouseover는 버블링이 발생한다. 즉, 자식 요소로 마우스가 이동하면 그 자식 요소에서 mouseover 이벤트가 발생하고, 그 이벤트가 부모 요소로 계속 올라간다. 결과적으로 부모에 붙인 mouseover 핸들러가 자식에서도 여러 번 실행된다.

mouseenter는 버블링이 발생하지 않는다. 오직 직접 그 요소 위로 마우스가 들어올 때만 한 번 발생하고, 자식 요소로 이동해도 새로 발생하지 않는다. 자식 요소는 자기 자신의 mouseenter 이벤트만 독립적으로 가진다.

 

코드로 확인하는 mouseover vs mouseenter

 

✗ 잘못된 코드: mouseover의 버블링
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 핸들러가 계속 실행된다.

 

✓ 올바른 코드: mouseenter로 버블링 차단
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 로직이 반복 실행되어서 깜빡거린다.

 

✗ mouseover로 구현: 깜빡거리는 드롭다운
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가 보였다가 사라졌다가를 반복한다. 사용자 입장에서는 메뉴가 깜빡거려서 거슬린다.

 

✓ mouseenter/mouseleave로 구현: 부드러운 드롭다운
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 vs mouseenter/mouseleave 비교표
특성 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와의 조합

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이 필요한 경우는 정말 드물고, 필요하다면 버블링을 명확히 이해한 후에 써야 한다. 이 글의 코드 예제를 참고해 현재 프로젝트의 호버 이벤트 핸들러를 한 번 점검해보면, 불필요한 이벤트 반복을 줄이고 사용자 경험도 개선할 수 있을 것이다.