드래그는 상태가 아니라 결과다: 클릭과 드래그를 구분하지 못했을 때 생기는 문제

드래그는 상태가 아니라 결과다: 클릭과 드래그를 구분하지 못했을 때 생기는 문제

2026년 1월 27일

offset 문제를 해결했지만, 이번에는 클릭이 드래그로 인식되는 문제가 남았습니다.

이 글은 floating button 인터랙션을 개선하며 깨달은 “드래그는 상태가 아니라 결과다”라는 관점과, threshold·이벤트 순서·상태 분리를 통해 클릭과 드래그의 의도를 구분한 과정을 정리한 기록을 공유해 봅니다.

문제 상황

floating button은

  • 클릭하면 사이드 패널을 열고
  • 드래그하면 화면 상하로 이동해야 한다.

offset 문제를 이해하고 적용한 뒤, 클릭 직후의 요소가 튀는 문제는 해결됐지만 새로운 문제가 드러났다.

  • 단순 클릭도 드래그로 인식됨
  • click / mouseup / mousedown 이벤트 순서 때문에 isDragging 조건이 실제로 의미가 없어짐
  • 드래그 중 텍스트 선택, 기본 브라우저 동작이 발생함

즉, “위치 계산(offset)은 맞아졌지만, 인터랙션의 의도(클릭 vs 드래그)는 구분되지 않는 상태”였다.

내가 처음 한 생각

처음에는 이렇게 생각했다. “드래그 중일 때만 onClick을 막으면 되지 않을까?”

그래서 if (!isDragging) 같은 조건을 추가했다.

  onClick={() => {
    if (!isDragging) {
      setIsOpen(!isOpen);
    }
  }}

하지만 이 판단에는 두 가지 착각이 있었다.

  1. 드래그 여부는 boolean 하나로 충분하다고 생각함
  2. 이벤트가 직관적인 순서로 동작할 거라고 가정함

실제로는

  • mouseup → click 순서 때문에
  • isDragging은 click 시점에는 이미 false가 되었고
  • 조건문은 논리적으로 항상 무력했다.

실제로 해 본 선택

접근을 완전히 바꿨다. 핵심 판단으로는 “드래그는 상태가 아니라 결과다.”라는 사고였다.

그래서 다음 선택을 했다.

  1. mousedown에서 즉시 드래그 시작하지 않음
  • 시작 좌표(startY)만 저장
  1. threshold 도입
  • 5px 이상 이동했을 때만 “드래그가 발생했다”고 판단
  1. 의도 판단용 상태 분리
  • isDragging: 현재 드래그 중인가 (UI 상태)
  • hasDragged: 실제로 드래그가 발생했는가 (의도 판단)
  1. 이벤트 순서 문제 해결
  • mouseup에서 hasDragged를 바로 리셋하지 않고 setTimeout(0)으로 click 이후로 지연
  1. 기본 동작 명시적 차단
  • e.preventDefault()로 텍스트 선택 방지

결과적으로 “드래그 중이었는가”가 아니라 “이 interaction에서 드래그가 발생했는가”를 기준으로 판단하게 됐다.

결과 (성공 / 실패)

결과: 성공

  • 단순 클릭 → threshold 미만 이동 → hasDragged = false → 사이드 패널 정상 토글
  • 드래그 → threshold 초과 → hasDragged = true → 위치 이동만 발생, 클릭 무시
  • 이벤트 순서 문제 해결
  • 브라우저 기본 동작 개입 제거

UI가 의도대로 반응하기 시작했다.

지금 다시 해본다면?

지금 다시 구현한다면, 이렇게 정리했을 것 같다.

“드래그 구현의 난이도는 좌표 계산이 아니라 의도 판별에 있다.”

그리고 초반부터 다음을 전제로 잡았을 것 같다.

  • 드래그는 mousedown에서 시작되지 않는다
  • 클릭과 드래그는 같은 제스처에서 갈라지는 결과다
  • boolean 하나로 인터랙션을 설명하려 들면 반드시 꼬인다

이제는 isDragging을 UI 표현용으로, hasDragged를 사용자 의도 판별용으로 명확히 역할 분리해서 설계했을 것이다.

FAQ

  • startY vs mousePositionRef
    • startY: 드래그 시작점 (고정)
    • mousePositionRef: 현재 위치 (변동)
    • 이 둘의 차이가 임계값인 5px을 넘어야 실제 드래그로 인정
    • 흐름은 다음과 같음
      1. onMouseDown: startY와 mousePositionRef 모두 초기화 (같은 값)
      2. onMouseMove: mousePositionRef만 계속 업데이트
      3. startY는 고정되어 있어서 “시작점에서 얼마나 움직였나” 비교 가능
      4. mousePositionRef는 실시간 위치라서 요소 위치 계산에 사용

실제 변경 내역