내가 기존 작성했던 코드

import { useRef, RefObject } from 'react';
import throttle from 'lodash.throttle';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';

const DRAG_PRESS_DELAY = 500 as const;
const THROTTLE_DELAY = 150 as const;

/**
 * useDraggable
 * Enables press-and-hold dragging on the given element ref.
 * - Hold mouse down for DRAG_PRESS_DELAY ms to activate drag.
 * - Moves the element by directly setting style.left/top.
 * - Cleans up event listeners on unmount.
 */
export function useDraggable(ref: RefObject<HTMLDivElement | null>) {
    const isDraggable = useRef(false);
    const isDragging = useRef(false);
    const timer = useRef<number | null>(null);
    const startPos = useRef({ x: 0, y: 0 });
    const boxRect = useRef<DOMRect | null>(null);

    const isDragged = useRef(false);

    // `useLayoutEffect`를 사용한 이유 [docs/useDraggable.md] -> 이후 useGSAP로 대체
    useGSAP(
        (_context, contextSafe) => {
            const element = ref.current;
            if (!element) return;
            if (!contextSafe) return;

            // Mouse move handler
            const onMouseMove = contextSafe((event: MouseEvent) => {
                if (!isDraggable.current || !boxRect.current) return;
                isDragging.current = true;
                const dx = event.clientX - startPos.current.x;
                const dy = event.clientY - startPos.current.y;
                element.style.left = `${boxRect.current.left + dx}px`;
                element.style.top = `${boxRect.current.top + dy}px`;
            });

            // Mouse down handler (throttled)
            const onMouseDown = throttle(
                contextSafe((event: MouseEvent) => {
                    // start press timer for drag activation
                    isDraggable.current = false;
                    timer.current = window.setTimeout(() => {
                        isDraggable.current = true;
                        timer.current = null;
                        isDragged.current = true;
                    }, DRAG_PRESS_DELAY);

                    // record initial positions
                    boxRect.current = element.getBoundingClientRect();
                    startPos.current = { x: event.clientX, y: event.clientY };

                    gsap.to(ref.current, {
                        scale: 1.03,
                        duration: 0.5,
                        delay: 0.2,
                        cursor: 'grabbing',
                        onInterrupt: () => {
                            gsap.set(ref.current, { scale: 1 });
                        },
                        overwrite: true,
                    });

                    document.addEventListener('mousemove', onMouseMove);
                }),
                THROTTLE_DELAY,
            );

            // Mouse up handler
            const onMouseUp = contextSafe(() => {
                isDraggable.current = false;
                isDragging.current = false;
                document.removeEventListener('mousemove', onMouseMove);
                if (timer.current) {
                    clearTimeout(timer.current);
                    timer.current = null;
                }

                gsap.to(ref.current, {
                    scale: 1,
                    duration: 0.5,
                    delay: 0.2,
                    cursor: 'pointer',
                    overwrite: true,
                    onInterrupt: () => {
                        gsap.set(ref.current, { scale: 1 });
                    },
                });
            });

            // Attach listeners
            element.addEventListener('mousedown', onMouseDown);
            document.addEventListener('mouseup', onMouseUp);

            // Cleanup
            return () => {
                element.removeEventListener('mousedown', onMouseDown);
                document.removeEventListener('mouseup', onMouseUp);
                document.removeEventListener('mousemove', onMouseMove);
                if (timer.current) {
                    clearTimeout(timer.current);
                    timer.current = null;
                }
                // Cancel any pending throttle
                onMouseDown.cancel();
            };
        },
        { dependencies: [ref], scope: ref },
    );

    return { isDragging: isDragging, isDragged: isDragged };
}

아래에 지금 구현을 더 매끄럽고 고성능으로 다듬을 수 있는 몇 가지 포인트를 비교 중심으로 정리해 보았습니다.


1. 이벤트 API: Mouse vs Pointer

항목 **기존 (**mousedown 등) **제안 (**pointerdown 등)
지원 디바이스 오직 마우스 마우스·터치·펜 모두 한 번에 지원
코드량·중복 별도 touchstart/touchmove 필요 단일 이벤트로 통합 → 핸들러 등록·해제 로직 절감
브라우저 호환성 비교적 넓음 IE11 제외한 최신 브라우저에서 모두 지원

강한 의견: 멀티터치 대응이 필요하다면 pointer* 계열로 전환하세요. 등록·해제 로직이 한 번으로 줄어들어 코드도 더 간결해집니다.


2. 이동 계산:

left/top

vs

transform: translate()

항목 기존 (style.left/top) 제안 (transform: translate3d)
레이아웃 재계산(Layout) 매 프레임마다 발생 → 성능 저하 위험 GPU 가속, 레이아웃 변경 없음 → 페인트·컴포지트 위주 처리
부드러움 간헐적 깜박임·저하 훨씬 부드러운 움직임 보장
코드 변경 element.style.left = ... 그대로 element.style.transform = 'translate3d(dx,dy,0)' 적용 필요

추천: 드래그 시에는 translate 쪽이 표준입니다. CSS 로 will-change: transform; 을 걸어두면 더 부드러워집니다.


3. 스로틀링:

lodash.throttle

vs

requestAnimationFrame