정의
요소를 직접 끌어서 옮기거나 크기를 조절하는 UI(커스텀 드래그·리사이즈)를 만들 때, 마우스에서 잘 되던 코드가 모바일 터치에서 자꾸 깨지는 문제를 다루는 글이다.
핵심 도구는 Pointer Events다. 이것은 마우스·터치·펜을 하나의 이벤트 체계로 묶은 브라우저 API로, 누름은 pointerdown, 움직임은 pointermove, 뗌은 pointerup, 그리고 브라우저가 제스처를 가로채 취소함은 pointercancel로 들어온다. 모바일에서 안정적으로 동작하려면 이 위에 네 가지를 함께 설계해야 한다: 입력 타입(마우스냐 터치냐)별로 언제 드래그를 시작할지 정하는 정책, pointercancel이 왔을 때의 상태 복원, touch-action(브라우저 기본 스크롤·줌 동작을 끌지 정하는 CSS 속성), 그리고 마우스가 없어 hover(포인터를 올려두는 상태)를 못 쓰는 터치 기기용 대체 UI다.
왜 필요한가
문제 상황부터 보면 이렇다. 브라우저는 터치 화면에서 스크롤·줌·길게 눌러 뜨는 메뉴(롱프레스 메뉴)를 기본으로 우선한다. 그래서 앱이 "이 손잡이 영역에서는 내가 제스처를 가져가겠다"고 선언하지 않으면, 드래그 도중 브라우저가 스크롤로 판단해 pointerup(뗌) 없이 pointercancel(취소)만 보내버려 드래그가 어정쩡하게 끊긴다. 또 마우스 hover에만 의존해 보여주던 손잡이는, hover 개념이 애매한 터치에서 깜빡이거나 사라지지 않는다.
이런 증상은 원인이 여러 겹으로 겹쳐 나타나므로, 아래처럼 원인별(활성화 정책·취소 처리·touch-action·hover 대체)로 나눠 하나씩 잡아야 한다.
동작 원리
pointerdown:event.pointerType으로"mouse"vs"touch"분기. 마우스는 작은 이동(예: 4px)으로 즉시 세션 시작, 터치는 롱프레스(예: 220ms) 또는 더 큰 이동(예: 10px, 지터 허용) 후 시작.- 세션 수명:
AbortControllersignal에pointermove,pointerup,pointercancel을 모두 등록. cancel은 스크롤 스틸·전화·멀티터치로pointerup없이 종료될 때 — 커밋 없이 복원. touch-action: none: 드래그/리사이즈 핸들 CSS. 없으면 브라우저가 세로 스크롤을 가져간다.contextmenu: 드래그 세션 중windowcapture-phase에서stopPropagation으로 Android 롱프레스 메뉴 차단. 마우스 우클릭(button !== 0)은 세션을 만들지 않으면 영향 없음.- Coarse hover:
@media (hover: none) and (pointer: coarse)에서 hover-리빌 UI를 상시 표시하거나 별도 탭 경로 제공. 터치는 sticky-hover로 "탭 → 나타남 → 다른 곳 탭 → 사라짐"이 반복된다. - Click 억제: 드래그 종료 후
onceclick 리스너는 터치에 trailingclick이 없어 다음 탭까지 삼킬 수 있음 — 짧은 timeout으로 해제.
실무 적용
외부 클릭 닫기는 mousedown만이 아니라 pointerdown 도 처리한다.
트레이드오프
| 선택 | 이득 | 비용 |
|---|---|---|
| 터치 롱프레스 활성화 | 스크롤과 충돌 감소 | 드래그 시작이 약간 늦음 |
touch-action: none | 제스처 소유 | 해당 영역 스크롤 불가 — 히트존 최소화 |
| coarse 상시 표시 | 깜빡임 제거 | 화면 밀도·시각적 노이즈 |
| HTML5 DnD API | 브라우저 네이티브 | 터치·커스텀 UX 제약 큼 |
사용하면 안 되는 경우
- 전체 페이지에
touch-action: none— 스크롤 마비. pointercancel없이pointerup만 처리 — 고착 상태.- hover-only 핸들만 두고 coarse 폴백 없음.
흔한 실수
- 데스크탑과 동일한 4px 이동 임계값을 터치에 적용.
defaultPrevented로 이벤트 억제 검증 — 다른 핸들러가 항상preventDefault하면 신호가 없음; bubble-phase probe 사용.- jsdom에서
scrollBy/elementFromPoint미구현으로 테스트 실패 — 환경 stub.
관련 개념
- html5-drag-and-drop — 네이티브 DnD vs Pointer Events
- selectionchange-floating-ui — 선택·플로팅 UI와 포인터