/ 2026.07.28
마커(버튼) 위에서 패닝 동작이 수행되지 않는 버그 수정기
직접 작성 후 AI로 정리한 내용입니다.
맵 뷰어에서 클릭하면 팝오버가 뜨는 마커 위에 마우스를 올리고 드래그하면, 화면 패닝(panning)이 동작하지 않는 버그가 있었다. 클릭 핸들러가 없는 객체 위에서는 문제가 없었다.
원인 1 — 마커의 stopPropagation이 패닝을 끊는다
마커 버튼에는 onPointerDown={(e) => e.stopPropagation()}이 걸려 있었다. pointer 이벤트는 누른 지점의 요소에서 발생한 뒤 DOM 트리를 따라 버블링되는데(버튼 → 오버레이 → 맵 컨테이너), 이 stopPropagation이 버블링을 버튼에서 끊어 컨테이너의 onPointerDown이 실행되지 않았다. 패닝은 컨테이너가 담당한다 — down에서 시작점을 기록하고, move에서 이동량을 계산해 화면을 옮긴다. 시작점 기록 자체가 안 되니 마커 위에서는 패닝이 시작될 수 없었다.
원인 2 — 그렇다고 stopPropagation을 지우면 클릭이 죽는다
stopPropagation을 제거하면 패닝은 살아나지만, 이번엔 클릭 시 팝오버가 안 열렸다. 컨테이너의 down 핸들러가 e.currentTarget.setPointerCapture(e.pointerId)를 호출하고 있었기 때문이다. capture가 걸리면 그 순간부터 해당 포인터의 이벤트는 컨테이너를 타겟으로 발생한다. 그러면 pointerdown의 타겟은 버튼인데 pointerup의 타겟은 컨테이너가 되고, UI Events 명세에 따라 click 이벤트는 두 타겟의 가장 가까운 공통 조상 — 즉 컨테이너에서 합성된다. 버튼의 onClick은 영영 실행되지 않는다.
정리하면: stopPropagation을 유지하면 패닝이 죽고, 제거하면 클릭이 죽는다. 단순한 방법으로는 두 요구를 동시에 만족할 수 없는 구조였다.
해결 — capture를 "드래그 판명 시점"으로 지연
요구를 다시 정의하면 이렇다.
- 클릭이면 버튼의 click이 정상 실행되어야 한다 — 컨테이너가 타겟을 가로채면 안 된다.
- 드래그면 포인터가 컨테이너 밖으로 나가도 move/up을 계속 받아야 한다 — capture가 필요한 이유.
즉 capture는 필요하되, down 시점이 아니라 드래그로 판명된 순간에만 걸어야 한다. 클릭과 드래그의 구분 기준은 마우스 이동량이다.
- down에서는 시작점만 기록하고 아무것도 확정하지 않는다.
- move에서 이동량이 임계값을 넘으면 그때 capture를 걸고 패닝을 시작한다.
- 임계값을 넘지 않고 up이 오면 capture가 걸린 적이 없으므로 up의 타겟이 버튼으로 유지되고, click이 정상 합성된다.
const PAN_DRAG_THRESHOLD_PX = 5;const handlePointerMove = useCallback((e) => { if (!dragRef.current) return; if (e.buttons === 0) { // up을 다른 요소가 삼킨 경우 — 버튼 뗀 채 패닝 시작 방지 dragRef.current = null; return; } const dx = e.clientX - dragRef.current.startX; const dy = e.clientY - dragRef.current.startY; if (!dragRef.current.active) { // 임계값 전 = 아직 클릭 후보. capture를 걸지 않아야 원래 타겟에서 click이 합성된다. if (Math.hypot(dx, dy) < PAN_DRAG_THRESHOLD_PX) return; dragRef.current.active = true; // ← 이 순간이 "드래그로 판명" e.currentTarget.setPointerCapture(dragRef.current.pointerId); setIsPanning(true); } setPanOffset(/* 시작 오프셋 + 이동량, clamp — 기존 그대로 */);}, [...]);
마무리로 up 핸들러의 releasePointerCapture는 hasPointerCapture 가드로 감싸 클릭 경로(capture 미발생)에서 release가 호출되지 않게 했고, 마커 버튼의 pointer 계열 stopPropagation은 제거해 down이 컨테이너까지 도달하게 했다.