포인터 이벤트: 통합 입력 처리

포인터 이벤트란

과거에는 오직 마우스 이벤트(mousedown, mouseup, mousemove 등)만 존재했습니다.
터치 디바이스가 보편화되면서 기존 마우스 이벤트로는 지원할 수 없는 멀티터치와 같은 새로운 입력 기능이 필요해졌고, 이를 위해 별도의 터치 이벤트(touchstart, touchend, touchmove 등)가 도입되었습니다. 그러나 터치와 마우스를 동시에 지원하려면 코드가 복잡해졌고, 스타일러스나 펜 입력과 같은 다른 장치의 특성을 고려하기엔 한계가 있었습니다. 이러한 문제를 해결하고자 Pointer Events 표준이 등장했습니다.

  • 통합성: 마우스, 터치, 펜 등 다양한 입력 장치에 대해 동일한 이벤트 핸들러를 사용할 수 있습니다.
  • 멀티터치 지원: 각 포인터에 고유한 pointerId를 부여하여 여러 터치 입력을 개별적으로 추적할 수 있습니다.
  • 포인터 캡쳐: 특정 요소에 입력 포인터를 “고정”하여, 포인터가 문서의 다른 영역으로 이동하더라도 해당 요소가 계속해서 이벤트를 수신할 수 있습니다.

포인터 이벤트의 주요 이벤트

Pointer Event 유사한 마우스 이벤트
pointerdown mousedown
pointerup mouseup
pointermove mousemove
pointerover mouseover
pointerout mouseout
pointerenter mouseenter
pointerleave mouseleave

주요 속성

  • pointerId: 각 포인터에 대한 고유 식별자. 여러 터치나 스타일러스 입력을 구분할 수 있습니다.
  • pointerType: 입력 장치의 종류를 나타냅니다. (예: "mouse", "touch", "pen")
  • isPrimary: 다중 입력 중 기본(primary) 포인터 여부를 나타내며, 일반적으로 첫 번째 터치에 해당합니다.
  • 좌표: clientX/Y, pageX/Y, offsetX/Y 등 기존 속성 지원
  • 추가 정보: pressure (압력, 0~1), tiltX/Y (펜 기울기).

포인터 이벤트를 활용한 드래그 앤 드롭 구현

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>포인터 이벤트: 드래그와 상태</title>
  <style>
    #dragBox {
      width: 80px; height: 80px; background: #42a5f5; color: white;
      position: absolute; top: 50px; left: 50px; text-align: center; line-height: 80px;
      touch-action: none; cursor: move;
    }
    #status { margin-top: 150px; }
  </style>
</head>
<body>
  <div id="dragBox">드래그</div>
  <div id="status"></div>
  <script>
    const dragBox = document.getElementById('dragBox');
    const status = document.getElementById('status');

    dragBox.addEventListener('pointerdown', (e) => {
      e.preventDefault();
      dragBox.setPointerCapture(e.pointerId);

      const rect = dragBox.getBoundingClientRect();
      const shiftX = e.clientX - rect.left;
      const shiftY = e.clientY - rect.top;

      status.textContent = `포인터: ${e.pointerType}, ID: ${e.pointerId}`;

      const moveBox = (moveEvent) => {
        dragBox.style.left = `${moveEvent.clientX - shiftX}px`;
        dragBox.style.top = `${moveEvent.clientY - shiftY}px`;
      };

      dragBox.addEventListener('pointermove', moveBox);
      dragBox.addEventListener('pointerup', () => {
        dragBox.removeEventListener('pointermove', moveBox);
        status.textContent = '드래그 종료';
      }, { once: true });
    });

    dragBox.ondragstart = () => false;
  </script>
</body>
</html>
  • setPointerCapture: 포인터 입력을 dragBox에 고정하여 요소 밖 이동에도 이벤트 수신
  • pointerType: 마우스, 터치, 펜 여부 표시
  • touch-action: none: 터치 기본 동작(스크롤, 확대/추소 등) 차단

포인터 캡처를 이용한 슬라이더 구현

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>포인터 이벤트: 슬라이더</title>
  <style>
    #track {
      width: 400px; height: 20px; background: #e0e0e0; position: relative;
      margin: 50px; border-radius: 10px;
    }
    #knob {
      width: 30px; height: 30px; background: #ff5722; border-radius: 50%;
      position: absolute; top: -5px; left: 0; touch-action: none; cursor: pointer;
    }
    #value { font-size: 18px; }
  </style>
</head>
<body>
  <div id="track">
    <div id="knob"></div>
  </div>
  <div id="value">0%</div>
  <script>
    const knob = document.getElementById('knob');
    const track = document.getElementById('track');
    const value = document.getElementById('value');

    knob.addEventListener('pointerdown', (e) => {
      e.preventDefault();
      knob.setPointerCapture(e.pointerId);

      const trackRect = track.getBoundingClientRect();
      const knobRect = knob.getBoundingClientRect();
      const shiftX = e.clientX - knobRect.left;

      const updateKnob = (moveEvent) => {
        let newLeft = moveEvent.clientX - trackRect.left - shiftX;
        newLeft = Math.max(0, Math.min(newLeft, trackRect.width - knobRect.width));
        knob.style.left = `${newLeft}px`;
        const percent = Math.round((newLeft / (trackRect.width - knobRect.width)) * 100);
        value.textContent = `${percent}%`;
      };

      knob.addEventListener('pointermove', updateKnob);
      knob.addEventListener('pointerup', () => {
        knob.removeEventListener('pointermove', updateKnob);
      }, { once: true });
    });
  </script>
</body>
</html>
  • setPointerCapture: 트랙 밖으로 벗어나도 knob이 이벤트를 수신
  • Math.max/min: 슬라이더 트랙 내 이동 범위 제한

포인터 이벤트 주의점

  • 기본 동작 방지: pointercancel 이벤트 방지를 위한 touch-action: none과 ondragstart = () => false 설정