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)
- useColorScheme — 색상 스킴 감지
- useElementSize — 요소 크기 관찰