mouseover/mouseout vs mouseenter/mouseleave

mouseover/mouseout: 세부 동작과 relatedTarget

mouseover

마우스 커서가 어떤 요소 위에 들어왔을 때 발생합니다.

  • event.target: 현재 마우스가 위치한 요소
  • event.relatedTarget: 마우스가 이전에 있던 요소(창 밖에서 진입 시 null)

mouseout

마우스 커서가 요소를 벗어날 때 발생합니다.

  • event.target: 마우스가 벗어난 요소
  • event.relatedTarget: 포인터가 이동한 새로운 요소 (창 밖일 경우 null)

자식 요소 간 이동도 이벤트로 인식되며, 버블링이 발생해 상위 요소에서도 감지 가능합니다.

예제: 마우스 이동 경로 추적하기

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>마우스 이동 경로 추적</title>
  <style>
    #tracker { width: 400px; height: 200px; border: 2px solid #444; position: relative; }
    #inner { width: 100px; height: 100px; background: #e0f7fa; position: absolute; top: 50px; left: 150px; }
    #log { margin-top: 10px; width: 400px; height: 100px; }
  </style>
</head>
<body>
  <div id="tracker">컨테이너
    <div id="inner">내부 요소</div>
  </div>
  <textarea id="log" readonly></textarea>
  <script>
    const tracker = document.getElementById('tracker');
    const log = document.getElementById('log');

    function logEvent(e) {
      const target = e.target.id || e.target.tagName;
      const related = e.relatedTarget ? (e.relatedTarget.id || e.relatedTarget.tagName) : '외부';
      log.value = `${e.type}: ${target} -> ${related}\n${log.value}`;
    }

    tracker.addEventListener('mouseover', logEvent);
    tracker.addEventListener('mouseout', logEvent);
  </script>
</body>
</html>

mouseenter/mouseleave: 단순화된 요소 진입 / 탈출

  • mouseentermouseleave는 mouseover/mouseout과 유사하게 동작하지만, 자식 요소 간의 이동은 무시됩니다.
  • 버블링이 발생하지 않으므로 이벤트 위임(event delegation)을 사용할 수 없다는 단점이 있지만, 내부 이동에 따른 불필요한 이벤트 발생을 막아줍니다.

예제: 단순 요소 이동 사용

<!-- index.html -->
<div id="hoverBox" style="width:300px; height:200px; background-color: #f0f0f0; margin-bottom:10px; position: relative;">
  마우스를 올려보세요.
  <div style="width:150px; height:100px; background-color: #cce5ff; position: absolute; top:50px; left:75px;">
    내부 박스
  </div>
</div>
<p id="status"></p>

<script>
  const hoverBox = document.getElementById('hoverBox');
  const status = document.getElementById('status');

  hoverBox.addEventListener('mouseenter', () => {
    status.textContent = 'mouseenter: 박스에 진입';
  });

  hoverBox.addEventListener('mouseleave', () => {
    status.textContent = 'mouseleave: 박스에서 탈출';
  });
</script>

버블링이 발생하지 않으므로, 부모 요소에서 자식 요소로 이동해도 이벤트가 발생하지 않습니다.

빠른 마우스 이동과 누락 처리

브라우저는 성능을 위해 마우스의 위치를 일정 간격으로 체크합니다. 이 때문에 마우스가 매우 빠르게 이동하면 중간에 있는 요소들을 건너뛰게 될 수 있습니다. 예를 들어, 두 요소 사이의 mouseover/mouseout 이벤트는 건너뛰어 바로 첫 요소에서 마지막 요소로 이벤트가 발생할 수 있습니다.

따라서 이벤트 처리 시 관련된 요소(relatedTarget)가 null일 수 있음을 염두에 두고 예외 처리를 하는 것이 좋습니다.

mouseover/mouseout: 이벤트 위임 활용

mouseenter/mouseleave는 버블링하지 않기 때문에, 많은 자식 요소에 대한 처리가 필요한 경우 이벤트 위임을 적용하기 어렵습니다.
대신, mouseover와 mouseout을 활용해 이벤트 위임을 구현할 수 있습니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>메뉴 항목 강조</title>
  <style>
    #menu { padding: 0; list-style: none; width: 200px; border: 1px solid #ddd; }
    li { padding: 10px; cursor: pointer; }
    .highlight { background: #fff176; }
  </style>
</head>
<body>
  <ul id="menu">
    <li>항목 1</li>
    <li>항목 2 <span>(부가 정보)</span></li>
    <li>항목 3</li>
  </ul>
  <script>
    const menu = document.getElementById('menu');
    let current = null;

    menu.addEventListener('mouseover', (e) => {
      const li = e.target.closest('li');
      if (!li || li === current) return;

      if (current) current.classList.remove('highlight');
      li.classList.add('highlight');
      current = li;
    });

    menu.addEventListener('mouseout', (e) => {
      if (!current) return;
      const related = e.relatedTarget;
      if (related && current.contains(related)) return;

      current.classList.remove('highlight');
      current = null;
    });
  </script>
</body>
</html>