useDrag

useDrag (드래그 제스처)

useDrag 훅은 요소의 드래그 제스처를 처리해요. 마우스와 터치를 모두 지원하며 축 제약, 탭 감지, 스와이프·드래그 스크롤 같은 다양한 패턴을 구현할 수 있어요.

출처: 문서

본문

useDrag 훅을 이용해 요소를 드래그할 수 있게 만들 수 있어요. useDrag는 상태 객체를 인자로 받는 핸들러를 받고, 이 핸들러는 제스처가 진행되는 동안 호출돼요. 반환값의 ref를 대상 요소에 전달하고, active로 드래그가 진행 중인지 확인할 수 있어요.

import { useRef, useState } from 'react';
import { Code, Group, Text } from '@mantine/core';
import { useDrag } from '@mantine/hooks';

function Demo() {
  const posRef = useRef({ x: 0, y: 0 });
  const startPosRef = useRef({ x: 0, y: 0 });
  const [pos, setPos] = useState({ x: 0, y: 0 });

  const { ref, active } = useDrag((state) => {
    if (state.first) {
      startPosRef.current = { ...posRef.current };
    }
    const newPos = {
      x: startPosRef.current.x + state.movement[0],
      y: startPosRef.current.y + state.movement[1],
    };
    posRef.current = newPos;
    setPos(newPos);
  });

  return (
    <Group>
      <div ref={ref} style={{ padding: 40, border: '1px solid gray' }}>Drag me</div>
      <Text>Position: {`{ x: ${Math.round(pos.x)}, y: ${Math.round(pos.y)} }`}</Text>
    </Group>
  );
}

축 제약 (Axis constraint)

axis 옵션으로 이동을 단일 축으로 제한할 수 있어요. axis를 'x'나 'y'로 설정하면 고정 제약이 적용되고, 'lock'으로 설정하면 axisThreshold를 초과한 뒤 이동량이 더 큰 축으로 잠금돼요.

const { ref: xRef, active: xActive } = useDrag(handler, { axis: 'x' });
const { ref: yRef, active: yActive } = useDrag(handler, { axis: 'y' });

탭과 드래그 구분하기 (Distinguishing taps from drags)

filterTaps를 활성화하면 마지막 상태에 tap 속성이 포함되고, 총 이동 거리가 tapThreshold(기본값 3px)보다 작으면 true가 돼요. threshold와 함께 사용하면 같은 요소에서 클릭과 드래그를 구분할 수 있어요.

useDrag(handler, { filterTaps: true, threshold: 5 });

스와이프로 닫기 (Swipe to dismiss)

마지막 이벤트의 movement와 velocity를 사용해 항목을 닫을지 결정할 수 있어요. 이 패턴은 알림(notification)에 잘 맞아요.

const { ref, active } = useDrag(
  (state) => {
    if (state.last) {
      const shouldDismiss =
        Math.abs(state.movement[0]) > 120 || state.velocity[0] > 0.5;
      if (shouldDismiss) {
        setDismissed(true);
        setTimeout(() => onDismiss(notification.id), 300);
      } else {
        setOffset(0);
      }
    } else {
      setOffset(state.movement[0]);
    }
  },
  { axis: 'x', threshold: 5, filterTaps: true }
);

드래그 스크롤 (Drag to scroll)

컨테이너의 scrollLeft에 delta를 적용하면 드래그-투-스크롤(drag-to-scroll) 상호작용을 만들 수 있어요.

const { ref, active } = useDrag(
  (state) => {
    if (scrollRef.current) {
      scrollRef.current.scrollLeft -= state.delta[0];
    }
  },
  { axis: 'x', filterTaps: true, threshold: 5 }
);

터치 지원 (Touch support)

이 훅은 마우스와 터치를 모두 자동으로 처리하는 Pointer Events API를 사용해요. 터치 기기에서는 브라우저가 터치 드래그를 스크롤로 해석하지 않도록 드래그 가능한 요소에 touch-action: none을 설정하세요.

.draggable {
  touch-action: none;
}

한 축에서 드래그하면서 다른 축에서 스크롤을 허용하려면(예: 세로 스크롤 + 가로 드래그) touch-action: pan-y 또는 touch-action: pan-x를 사용해요.

정의 (Definition)

type Vector2 = [number, number];

interface UseDragState {
  /** Current pointer position */
  xy: Vector2;
  /** Position where the gesture started */
  initial: Vector2;
  /** Displacement from start, respects axis constraint */
  movement: Vector2;
  /** Change since previous event */
  delta: Vector2;
  /** Absolute distance per axis */
  distance: Vector2;
  /** Movement direction per axis: -1, 0 or 1 */
  direction: Vector2;
  /** Speed per axis in px/ms */
  velocity: Vector2;
  /** Time since drag started in ms */
  elapsedTime: number;
  /** `true` on the first handler call */
  first: boolean;
  /** `true` on the last handler call (pointer released or canceled) */
  last: boolean;
  /** `true` while the gesture is ongoing */
  active: boolean;
  /** `true` when the gesture qualifies as a tap (requires `filterTaps`) */
  tap: boolean;
  /** `true` when the gesture was interrupted by a `pointercancel` event */
  canceled: boolean;
  /** Function to programmatically cancel the current gesture */
  cancel: () => void;
  /** The source pointer event */
  event: PointerEvent;
}

interface UseDragOptions {
  /** Constrain movement to an axis, `'lock'` locks to whichever axis has more movement */
  axis?: 'x' | 'y' | 'lock';
  /** Movement in px to determine lock axis, default `1` */
  axisThreshold?: number;
  /** Enable tap detection on the last event, default `false` */
  filterTaps?: boolean;
  /** Max displacement in px to be considered a tap, default `3` */
  tapThreshold?: number;
  /** Min displacement before drag activates, default `0` */
  threshold?: number | Vector2;
  /** Enable or disable the hook, default `true` */
  enabled?: boolean;
}

interface UseDragReturnValue {
  /** Ref callback to attach to the target element */
  ref: React.RefCallback;
  /** `true` while a drag gesture is active */
  active: boolean;
}

function useDrag(
  handler: (state: UseDragState) => void,
  options?: UseDragOptions,
): UseDragReturnValue

Exported types

UseDragState, UseDragOptions, UseDragReturnValue 타입은 @mantine/hooks 패키지에서 내보내져요. 애플리케이션에서 다음과 같이 불러올 수 있어요.

import type { UseDragState, UseDragOptions, UseDragReturnValue } from '@mantine/hooks';

더 알아보기 (Learn more)