bkdragon's log

드래그 가능한 모달창 만들기 본문

React

드래그 가능한 모달창 만들기

bkdragon 2024. 11. 11. 21:50

브라우저 처럼 드래그로 이동이 가능한 모달창을 만들어보자.

기본 아이디어는 아래와 같다:

  1. 마우스로 모달창을 클릭한 순간 현재 마우스 위치와 모달의 좌측 상단 모서리와 차이를 저장하고 드래그 중임을 알려주는 flag 상태를 true로 만든다.
  2. flag 가 true가 되면, movemove 이벤트 리스너를 등록하고, 움직이는 위치로 위치 상태값을 업데이트 시킨다.

현재 마우스 위치와 모달의 좌측 상단 모서리와의 차이가 왜 필요한지는 예를 들면 쉽게 이해된다.

만약 모달의 현재 위치가 (100, 100) 이고, 마우스 클릭 위치가 (120, 130) 이라면
차이(dragOffset)는 (20, 30) 이 된다.
그리고 마우스를 (150, 160) 으로 이동 하면
새로운 모달 위치를 dragOffset 을 빼면 얻을 수 있다. (150-20, 160-30) = (130, 130)

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  children?: React.ReactNode;
}

interface Position {
  x: number;
  y: number;
}

const DraggableModal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
  const modalRef = useRef<HTMLDivElement>(null);
  const [isDragging, setIsDragging] = useState(false);
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
  const [dragOffset, setDragOffset] = useState<Position>({ x: 0, y: 0 });

  const handleMouseDown = useCallback(
    (e: React.MouseEvent) => {
      setIsDragging(true);
      setDragOffset({
        x: e.clientX - position.x,
        y: e.clientY - position.y,
      });
    },
    [position]
  );

  const handleMouseMove = useCallback(
    (e: MouseEvent) => {
      if (isDragging) {
        setPosition({
          x: e.clientX - dragOffset.x,
          y: e.clientY - dragOffset.y,
        });
      }
    },
    [isDragging, dragOffset]
  );

  const handleMouseUp = useCallback(() => {
    setIsDragging(false);
  }, []);

  useEffect(() => {
    if (isDragging) {
      window.addEventListener("mousemove", handleMouseMove);
      window.addEventListener("mouseup", handleMouseUp);
    }

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("mouseup", handleMouseUp);
    };
  }, [isDragging, handleMouseMove, handleMouseUp]);

  // 초기 위치를 중앙으로 설정
  useEffect(() => {
    if (isOpen && modalRef.current) {
      const x = window.innerWidth / 2 - modalRef.current.offsetWidth / 2;
      const y = window.innerHeight / 2 - modalRef.current.offsetHeight / 2;
      setPosition({ x, y });
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return createPortal(
    <div
      ref={modalRef}
      style={{
        position: "fixed",
        left: position.x,
        top: position.y,
        backgroundColor: "white",
        boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
        borderRadius: "4px",
        zIndex: 1000,
      }}
    >
      <div
        onMouseDown={handleMouseDown}
        style={{
          padding: "1rem",
          backgroundColor: "#f1f1f1",
          cursor: isDragging ? "grabbing" : "grab",
          borderBottom: "1px solid #ddd",
        }}
      >
        드래그 가능한 헤더
        <button
          onClick={onClose}
          style={{ float: "right", cursor: "pointer" }}
        >
          ✕
        </button>
      </div>
      <div style={{ padding: "1rem" }}>{children}</div>
    </div>,
    document.body
  );
};

export default DraggableModal;

보통 모달은 뒤에 보이지 않는 배경을 둬서 인터렉션을 막는데 이 모달창은 그렇지 않다. 여러개를 열 수도 있다. 추후에 최소화 기능을 추가해봐도 좋을 것 같다.