인턴

[ Troubleshooting🛠️ ] react-draggable 라이브러리 사용시 버튼 드래그 & 버튼 클릭 이벤트 기능 분리하기

choijming21 2025. 5. 14. 11:40

1.  문제 발생❓

  1. react-draggable에서 제공해주는 Draggable 기능을 일반 버튼에 추가했더니 드래그는 잘 작동하지만 원래의 onClick 이벤트가 제대로 발생하지 않음
  2. 드래그 기능과 버튼 클릭 이벤트를 분리했더니 웹에서는 드래그와 클릭이 모두 작동했지만, 모바일에서는 버튼 클릭 기능이 작동하지 않음

 

 

2.  원인 추론 🔎

드래그 이벤트가 클릭 이벤트를 가로채는 현상이 발생하고 있습니다. 이는 이벤트 전파(event propagation) 과정에서 드래그 이벤트 핸들러가 클릭 이벤트를 소비(consume)해버리는 이벤트 버블링 이슈로 판단됩니다.

 

< 기존 코드 >

import { useAtom } from 'jotai';
import { isRenderedAtom } from '@/components/Landing/state';
import * as S from './style';
import Draggable from 'react-draggable';

export default function ControlButton() {
  const [isRendered, setIsRendered] = useAtom(isRenderedAtom);

  return (
    <Draggable>
      <S.Button onClick={() => setIsRendered(!isRendered)}>
        {isRendered ? '숨기기' : '띄우기'}
      </S.Button>
    </Draggable>
  );
}

 

 

 

 

3.  해결 과정 📋

  • 드래그와 클릭 이벤트 분리
    • isDragging 상태를 추가하여 드래그 중인지 단순 클릭인지 구분
const [isDragging, setIsDragging] = useState(false);

const handleDrag = () => {
  setIsDragging(true);
};

const handleStop = () => {
  setTimeout(() => {
    setIsDragging(false);
  }, 100);
};

const handleClick = () => {
  if (isDragging) return;// 드래그 중이면 클릭 이벤트 무시
  setIsRendered((prev) => !prev);
};

 

 

  • 모바일 터치 이벤트 처리
    • 시도했던 방법 => onTouchEnd 추가 => 여전히 작동하지 않음
    • 최종 해결책 => onPointerUp 이벤트 사용 => 모바일에서도 터치 이벤트 작동!
    • 여기서 onPointerUp 이벤트란?
      • onPointerUp은 마우스 클릭, 터치, 펜 등 다양한 입력 방식을 통합적으로 처리하는 현대적인 이벤트 핸들러
      • 사용자가 버튼에서 손가락/마우스를 뗐을 때 발생하는 이벤트
      • 웹과 모바일 환경 모두에서 일관된 동작 보장
      • 별도의 분기 처리 없이 하나의 이벤트로 해결
<S.Button onPointerUp={handleClick}>
  {isRendered ? '이름\n숨기기' : '이름\n띄우기'}
</S.Button>

 

 

 

  • 하나 더 나아가서 코드 리뷰에서 메모리 관리에 관한 피드백을 받았습니다.
    • setTimeout() 함수 사용 시 컴포넌트가 언마운트되더라도 타이머가 계속 실행될 수 있어 메모리 누수가 발생할 수 있습니다. 이를 방지하기 위해 적절한 시점에 clearTimeout()을 호출하여 타이머를 정리해주어야 합니다.
    • useEffect의 클린업 함수를 통해 clearTimeout()을 호출하여 타이머 자원을 적절히 해제해주는 것이 좋습니다!

< 개선 된 최종 코드 >

import { useEffect, useRef, useState } from 'react';
import { useAtom } from 'jotai';
import { isRenderedAtom } from '@/components/Landing/state';
import * as S from './style';
import Draggable from 'react-draggable';

export default function ControlButton() {
  const [isRendered, setIsRendered] = useAtom(isRenderedAtom);
  const [isDragging, setIsDragging] = useState(false);
  const timeoutRef = useRef<number>();

  const handleStop = () => {
    clearTimeout(timeoutRef.current);
    timeoutRef.current = window.setTimeout(() => {
      setIsDragging(false);
    }, 100);
  };

  const handleClick = () => {
    if (!isDragging) setIsRendered((prev) => !prev);
  };

  useEffect(() => {
    return () => {
      clearTimeout(timeoutRef.current);
    };
  }, []);

  return (
    <Draggable onDrag={() => setIsDragging(true)} onStop={handleStop}>
      <S.Button onPointerUp={handleClick}>
        {isRendered ? '숨기기' : '띄우기'}
      </S.Button>
    </Draggable>
  );
}

 

 

 

 

4.  결론 💬

이벤트 버블링으로 인한 드래그와 클릭 이벤트 충돌 문제를 해결했습니다.

 

핵심 개선사항으로는

1) isDragging 상태를 통한 이벤트 구분

2) onPointerUp 이벤트 사용으로 크로스 플랫폼 호환성 확보

3) useEffect 클린업 함수를 활용한 메모리 누수 방지

 

이를 통해 react-draggable 라이브러리를 통해 웹과 모바일 환경 모두에서 드래그와 클릭 기능이 정상적으로 작동하는 사용자 경험을 제공할 수 있게 되었습니다.