jQuery / Method / .hover()

jQuery의 .hover()는 마우스 포인터가 요소 안으로 들어올 때와 밖으로 나갈 때의 함수를 연결하는 축약 메서드입니다. jQuery 3.3부터 사용 중단이 권고되어 새 코드에서는 .on("mouseenter", ...)와 .on("mouseleave", ...)를 사용합니다. 사용 중단 권고와 제거는 다르며, 아래 예제는 3.7.1과 4.0.0에서 두 방식을 실행해 확인했습니다.

두 함수 형식과 한 함수 형식

.hover() 공식 문서의 두 함수 형식은 첫 함수를 진입 때, 두 번째 함수를 이탈 때 호출합니다. 이벤트 이름 hover를 등록하는 방식이 아니라 mouseenter와 mouseleave를 함께 연결합니다.

기존 형식 대체 형식 의미
.hover(enter, leave) .on("mouseenter", enter).on("mouseleave", leave) 진입·이탈을 별도 함수로 처리
.hover(handler) .on("mouseenter mouseleave", handler) 하나의 함수가 두 이벤트에 실행

mouseenter는 요소 경계로 들어오는 이벤트입니다. 이미 부모 안에 있는 포인터가 그 안의 span으로 이동했다고 부모에 다시 진입한 것으로 처리하지 않습니다. 자식 이동에서도 전파되는 mouseover와 구분하면 불필요한 반복 실행을 줄일 수 있습니다.

기존 hover와 on을 비교하는 전체 실습

전체 코드를 UTF-8의 hover-demo.html로 저장하고 브라우저에서 엽니다. 공식 jQuery 파일 4.0.0을 CDN(Content Delivery Network, 콘텐츠 전송 네트워크)에서 읽을 인터넷 연결이 필요하며 별도 설치는 없습니다. 예제와 단일 함수 형식, 자식 이동·동적 요소·키보드·해제를 3.7.1과 4.0.0에서 실행했습니다.

Alpha는 기존 .hover()를, Beta는 상위 #cards에 위임한 .on()을 사용합니다. 두 카드의 내용은 항상 보이며 강조만 바뀝니다. Beta에는 focusin·focusout도 추가해 키보드 사용 시에도 강조합니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hover events and keyboard focus</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 24px auto; padding: 16px; }
    button { font: inherit; margin: 6px; }
    .card { display: block; padding: 20px; border: 2px solid teal; background: white; }
    .active { background: #d9f4ef; }
    .card:focus-visible { outline: 3px solid #af5f00; outline-offset: 3px; }
  </style>
</head>
<body>
  <h1>Compare legacy hover and on</h1>
  <button type="button" id="add">Add modern card</button>
  <button type="button" id="bind">Bind modern</button>
  <button type="button" id="unbind">Unbind modern</button>
  <button type="button" id="reset">Reset</button>
  <div id="cards">
    <button type="button" class="card legacy">Legacy <span>Alpha</span></button>
    <button type="button" class="card modern">Modern <span>Beta</span></button>
  </div>
  <p id="result" aria-live="polite"></p>
  <script>
    $(function () {
      const $cards = $("#cards");
      let legacyEntries = 0;
      let modernEntries = 0;
      function report() {
        $("#result").text(`legacy enters=${legacyEntries} | modern enters=${modernEntries}`);
      }
      $(".legacy").hover(function () {
        legacyEntries++;
        $(this).addClass("active");
        report();
      }, function () { $(this).removeClass("active"); });
      function bindModern() {
        $cards.off(".hoverLesson").on(
          "mouseenter.hoverLesson mouseleave.hoverLesson focusin.hoverLesson focusout.hoverLesson",
          ".modern", function (event) {
            const $card = $(this);
            if (event.type === "mouseenter") { $card.data("inside", true); modernEntries++; }
            if (event.type === "mouseleave") $card.data("inside", false);
            if (event.type === "focusin") $card.data("focused", true);
            if (event.type === "focusout") $card.data("focused", false);
            $card.toggleClass("active", Boolean($card.data("inside") || $card.data("focused")));
            report();
          }
        );
      }
      $("#bind").on("click", bindModern);
      $("#unbind").on("click", function () {
        $cards.off(".hoverLesson");
        $(".modern").removeClass("active").removeData("inside focused");
      });
      $("#add").on("click", function () {
        if (!$("#gamma").length) $('<button type="button" id="gamma" class="card modern">Modern <span>Gamma</span></button>').appendTo($cards);
      });
      $("#reset").on("click", function () {
        $("#gamma").remove();
        $(".card").removeClass("active").removeData("inside focused");
        legacyEntries = 0;
        modernEntries = 0;
        bindModern();
        report();
      });
      bindModern();
      report();
    });
  </script>
</body>
</html>

초기 진입 횟수는 모두 0입니다.

legacy enters=0 | modern enters=0
  1. 마우스를 Legacy Alpha 안으로 넣으면 배경이 바뀌고 legacy enters가 1 증가합니다. 카드 안에서 Alpha 글자의 span으로 옮겨도 추가 증가하지 않습니다.
  2. 카드 밖으로 나가면 강조가 사라집니다. Modern Beta에서도 modern enters가 진입마다 증가합니다.
  3. Add modern card를 누르면 Gamma가 추가됩니다. 이미 연결한 상위 위임 핸들러가 새 카드도 처리하므로 별도 등록 없이 진입 횟수가 증가합니다.
  4. Tab으로 Beta에 포커스를 옮기면 배경 강조와 포커스 외곽선이 나타납니다. 마우스만 밖으로 이동해도 포커스가 남아 있으면 배경을 유지합니다. 포인터와 포커스가 모두 떠나야 해제됩니다.
  5. Unbind modern은 Modern 카드 처리만 해제합니다. Alpha의 기존 hover는 계속 동작합니다.
  6. Bind modern을 반복해 눌러도 중복 실행되지 않습니다. Reset은 Gamma와 강조 상태를 없애고 횟수를 0으로 돌린 뒤 Modern 처리를 다시 연결합니다.

포인터와 포커스를 각각 inside·focused로 저장하고 둘 중 하나라도 활성 상태이면 강조합니다. 이 상태를 따로 관리하지 않고 mouseleave 때 무조건 강조를 지우면, 키보드 포커스가 남아 있는데도 시각 상태가 사라질 수 있습니다.

동적 요소와 이벤트 해제 범위

.on()의 위임 형식은 유지되는 상위 요소에 연결하고 후손 선택자를 지정합니다. 여기서는 #cards가 그대로 남아 있고 .modern 버튼만 처리합니다. 기존 .hover()는 등록할 때 선택된 요소에 직접 연결하므로, 나중에 추가한 요소가 자동으로 같은 핸들러를 얻는다고 가정하지 않습니다.

hoverLesson은 이벤트 이름 공간입니다. bindModern()은 .off(".hoverLesson")로 자신이 등록한 처리만 지운 뒤 다시 연결합니다. 인수 없는 .off()나 광범위한 .off("mouseenter mouseleave")는 다른 코드의 이벤트도 제거할 수 있으므로 이 예제처럼 범위를 좁힙니다.

일반 function 핸들러에서 this는 처리 대상 Modern 카드입니다. 안쪽 span이 실제 이벤트의 출발점일 수 있는 event.target과 구분합니다. 이 this를 사용하려면 화살표 함수로 무작정 바꾸지 않습니다.

단일 함수 확인과 입력 방식 선택

개발자 도구의 Console에서 다음 완전한 부분 코드를 실행하면 한 함수가 두 이벤트에 호출된다는 것을 확인할 수 있습니다. 임시 버튼을 문서에 넣지 않고 이벤트를 직접 발생시키며 끝에서 정리합니다. 실제 포인터 이동 검증은 위 전체 실습에서 수행합니다.

const events = [];
const $button = $("<button>Temporary</button>");
$button.hover(function (event) { events.push(event.type); });
$button.trigger("mouseenter").trigger("mouseleave");
console.log(events.join(", "));
$button.remove();
mouseenter, mouseleave

이 출력은 진입·이탈을 모두 받는다는 뜻이지 지속적인 마우스 이동을 매 순간 받는다는 뜻은 아닙니다. 함수 하나를 쓸 때도 event.type을 확인해 상태를 명확히 정하면, 외부 상태 변경 때문에 단순 toggle이 뒤집히는 문제를 피하기 쉽습니다.

터치 화면에서 마우스 hover만으로 필수 내용을 여는 동작을 기대하지 않습니다. 중요한 기능은 클릭·탭이나 키보드로도 접근할 수 있게 만들고, 장식적인 스타일만 바뀐다면 CSS :hover와 :focus-visible만으로 충분한지도 검토합니다. 이 예제는 카드 내용을 항상 보여 주고 강조만 바꿉니다.