2026
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec

/ 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를 "드래그 판명 시점"으로 지연

요구를 다시 정의하면 이렇다.

  1. 클릭이면 버튼의 click이 정상 실행되어야 한다 — 컨테이너가 타겟을 가로채면 안 된다.
  2. 드래그면 포인터가 컨테이너 밖으로 나가도 move/up을 계속 받아야 한다 — capture가 필요한 이유.

즉 capture는 필요하되, down 시점이 아니라 드래그로 판명된 순간에만 걸어야 한다. 클릭과 드래그의 구분 기준은 마우스 이동량이다.

  1. down에서는 시작점만 기록하고 아무것도 확정하지 않는다.
  2. move에서 이동량이 임계값을 넘으면 그때 capture를 걸고 패닝을 시작한다.
  3. 임계값을 넘지 않고 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 핸들러의 releasePointerCapturehasPointerCapture 가드로 감싸 클릭 경로(capture 미발생)에서 release가 호출되지 않게 했고, 마커 버튼의 pointer 계열 stopPropagation은 제거해 down이 컨테이너까지 도달하게 했다.