커서 위치를 따라 움직이는 커스텀 커서 UI를 구현하던 중, 모바일 환경에서 예상치 못한 문제가 발생했다.
터치가 발생한 이후, 커서가 특정 위치에 그대로 남아 움직이지 않는 현상이었다.

문제 상황
초기 구현은 단순히 mousemove 이벤트를 사용해 커서의 위치를 업데이트했다.
document.addEventListener("mousemove", (e) => {
// 커서 위치 계산
});
당연히 마우스를 움직이는 상황에서만 커서의 위치가 반영되겠지라고 생각했지만, 모바일 환경에서 테스트해보니 터치 시에도 mousemove 이벤트 핸들러가 실행되는 것을 알 수 있었다.
왜 터치 환경에서 mouse 이벤트가 발생할까?
과거 컴퓨팅 환경은 데스크톱 중심이었고, 입력 장치는 마우스 뿐이었다. 그러다 터치 입력 방식이 처음 등장했다. 당시 웹은 마우스 기반 이벤트 모델에 강하게 의존하고 있었다.
만약 터치 기기에서 마우스 이벤트를 아예 발생시키지 않으면, 기존 웹 사이트는 모바일 환경에서 작동하지 않게 된다.
따라서 브라우저는 터치 입력을 내부적으로 마우스 이벤트로 변환하여 발생시키는 방식을 통해, 기존 웹사이트가 모바일에서도 정상적으로 동작하도록 했다.
실제로 터치 시 다음과 같은 순서로 이벤트가 발생한다.
- touchstart
- touchmove
- touchend
- mouseover
- mousemove
- mousedown
- mouseup
- click
이 덕분에 기존 웹은 깨지지 않았지만, 터치와 마우스의 입력 특성 차이로 인해 예상치 못한 문제가 발생하기도 한다.
1차 해결: 모바일에서 커서 UI 숨기기
처음에 들었던 생각은 모바일 환경에선 커서 UI가 보이지 않게 하는 것이었다.
pointer 미디어 쿼리
CSS의 pointer 미디어 쿼리는 사용자가 마우스 같은 포인팅 장치를 쓰고 있는지 확인한다. 그 장치가 얼마나 정밀하게 조작할 수 있는지에 따라 스타일을 다르게 적용할 수 있다.
- pointer: fine → 마우스/트랙패드처럼 정밀한 포인팅 장치
- pointer: coarse → 손가락 기반 터치 입력
- pointer: none → 포인터 장치 없음
해결
그래서 pointer: fine 미디어 쿼리를 토해 정밀 포인팅 장치가 있는 환경에서만 커서 컴포넌트를 표시하도록 했다.
<div className="pointer-fine:block hidden">
{/* 커서 컴포넌트 */}
</div>
pointer: fine은 마우스나 트랙패드처럼 정밀한 포인팅 장치가 있는 환경에서만 매칭된다. 모바일의 터치 입력(pointer: coarse) 환경에서는 자동으로 숨겨진다.
이 방법으로 모바일에서의 UX 문제는 해결됐다.
하지만 이벤트 처리는 여전히 입력 장치와 상관없이 수행되고 있다.
2차 개선: pointer 이벤트로 전환
터치에 의해 발생하는 mousemove 이벤트는 무시하고자 했다.
터치 이벤트가 발생하면, 브라우저는 mouse 이벤트를 합성해서 새롭게 발생시킨다. 이러한 작업을 방지하려면 터치 이벤트에서 preventDefault()를 호출하는 방법이 있다.
element.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
},
{ passive: false } // touch 이벤트는 false여야 preventDefault 가능
);
또 다른 방법으로는 Pointer Event로 처리하는 방법이다.
Pointer Events API
pointer 이벤트는 마우스, 터치, 펜 입력을 하나의 통합 모델로 다룬다.
- pointermove
- pointerdown
- pointerup
이벤트의 pointerType(mouse | touch | pen) 속성 값을 통해 입력 장치를 구분할 수 있도록 한다.
JavaScript 레벨 대응: Pointer Events API는 왜 등장했을까?
터치와 마우스 이벤트가 공존하는 구조는 웹의 호환성을 지켜냈지만, 동시에 개발자에게 혼란을 남겼다.
- 터치 이벤트를 따로 처리해야 하고
- 브라우저는 여전히 마우스 이벤트를 발생시키며
- 같은 동작이 두 번 처리되는 문제나 예외 케이스가 발생했다
특히 하이브리드 기기(터치 + 마우스 지원)에서는 입력 모델이 더욱 복잡해졌다.
이러한 문제를 해결하기 위해 등장한 것이 Pointer Events API다.
Pointer Events는 마우스, 터치, 펜 입력을 하나의 이벤트 모델로 통합하면서도, pointerType을 통해 입력 장치를 명확히 구분할 수 있도록 설계되었다.
해결
커서 UI는 마우스 입력에만 반응하면 되기 때문에, pointermove를 사용하면서 pointerType으로 필터링했다.
useEffect(() => {
const root = document.getElementById("root");
if (!root) return;
const eventHandler = (e: PointerEvent) => {
if (e.pointerType !== "mouse") return;
const { w, h } = sizeRef.current;
positionRef.current = {
x: e.clientX - w / 2,
y: e.clientY - h / 2,
};
};
root.addEventListener("pointermove", eventHandler);
return () => root.removeEventListener("pointermove", eventHandler);
}, []);
터치 입력은 무시하고, 마우스 입력에 대해서만 위치를 업데이트하도록 했다.
참고로 트랙패드는 터치처럼 보이지만 브라우저에서는 마우스와 동일한 정밀 포인팅 장치로 취급되기 때문에, pointerType이 "mouse"로 들어온다. 따라서 데스크톱 환경에서는 자연스럽게 동작한다.
참고자료
https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/pointer
https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
https://web.dev/articles/mobile-touchandmouse