MouseEvent로 드래그 기능 구현하기

Sunny·2023년 5월 20일
0

Front End

목록 보기
2/2
post-thumbnail

현재 활동하고있는 대학생 연합 IT창업동아리 SOPT의 3주 단기해커톤에서 ‘드래그미' 서비스의 웹 프론트 개발자로 함께하게 되었다. 드래그미 서비스의 메인 기능은 서비스 명에서도 알 수 있듯이 드래그 기능이다. 나는 그 중에서도 타임 블럭들을 드래그 하면 시간을 계획 할 수 있는 컴포넌트를 담당하여 개발했다.

완성된 컴포넌트는 다음과 같다.

왜 Drag Event를 사용하지 않았나

드래그 앤 드랍 기능을 Mouse Event로 사용하게 된 이유에는, 먼저 Drag Event가 해당 컴포넌트의 기능과 적합하지 않다고 생각했기 때문이다.

먼저, javascript의 Drag Event에 대해서 살펴보자면

  1. dragstart
    사용자가 객체(object)를 드래그하려고 시작할 때 발생함.
  2. dragenter
    마우스가 대상 객체의 위로 처음 진입할 때 발생함.
  3. dragover
    드래그하면서 마우스가 대상 객체의 위에 자리 잡고 있을 때 발생함.
  4. drag
    대상 객체를 드래그하면서 마우스를 움직일 때 발생함.
  5. drop
    드래그가 끝나서 드래그하던 객체를 놓는 장소에 위치한 객체에서 발생함.
  6. dragleave
    드래그가 끝나서 마우스가 대상 객체의 위에서 벗어날 때 발생함.
  7. dragend
    대상 객체를 드래그하다가 마우스 버튼을 놓는 순간 발생함.

다음과 같이 7가지가 있다.

위에서도 확인 가능하듯이, 자바스크립트의 Drag Event는 어떠한 한 대상을 잡고, 그것을 drop 하는 과정에서 일어나는 event를 감지하기 위한 event들이었다.

따라서, 내가 구현하려는 드래그 기능을 개발하기 위해서는, dragstart로 드래그를 시작하고, 처음 드래그 하는 블럭이 다른 블럭 위로 진입할때 drag enter 이벤트를 감지하여 그 블럭의 색을 바꿔주는 방식으로 구현을 해야했다.

하지만 위와 같은 방식으로 드래그를 구현하면, 여러가지 문제점들이 발생했는데,

  1. 한 블럭 위에 드래그중인 블럭이 진입했는지 여부를 판별하여 이벤트를 발생시키기 때문에, 완전하게 마우스가 들어가지 않거나 하는 경우에는 이벤트 감지가 되지 않아 중간 중간 드래그 되지 않는 블럭이 발생하였다.
  2. 드래그 하고 있는 블럭에 대한 정보는 원하는 대로 다룰 수 있었지만, 드래그 하고있는 블럭이 올라가있는 블럭에 대한 정보들은 이벤트 객체에서 관리되지 않고 있어서 원하는대로 다룰수가 없었다.

나는 과정을 거쳐 Drag Event는 드래그앤 드랍에는 적합한 이벤트 였지만 현재 내가 구현하는 단순히 ‘드래그'로만 이루어진 기능에는 적합하지 않다고 판단했다.

그래서 나는 Mouse Event로 드래그 기능을 구현하기로 하였다!

Mouse Event로 드래그 기능 구현하기

먼저 나는 마우스 이벤트로 드래그 기능을 구현하기 위해 다음과 같은 세가지 mouse event를 사용했다.

  1. mouse down

    마우스 왼쪽 버튼을 누를 때 발생

  2. mouse over

    마우스가 컴포넌트 안에 들어올때 발생

  3. mouse up

    마우스 왼쪽 버튼을 누르고있다가 떼어냈을때 발생

드래그 이벤트와 비교하자면, mouse down은 drag start가 되는것이고, mouse over는 drag enter 혹은 over , mouse up은 drag end가 되는것이다.

전체적인 로직은 다음과 같다.

  1. mouse down 이벤트 발생 시 isDragging 변수를 true로 변경, startBlock의 id값과 endBlock의 id값을 저장. (endBlock의 id값을 저장하는 이유는 클릭 했을 시에도 블럭이 기록이 되어야 하기 때문)
  2. mouse를 이동하면서 endBlock을 수시로 업데이트 (isDraggin이 true일 경우에만)
  3. mouse up을 했을때 서버에 데이터 전송 및 start,end 블럭 초기화. isDragging false로 초기화

다음과 같은 마우스 이벤트의 로직을 커스텀 훅으로 작성하여 사용하였다.

import React, { useState } from 'react';

interface DragBlockHookArg {
  handleSubmit: () => void;
}

const INITIAL_BLOCK = -1;

function useDragBlock({ handleSubmit }: DragBlockHookArg) {
  const [startBlock, setStartBlock] = useState(INITIAL_BLOCK);
  const [endBlock, setEndBlock] = useState<number>(INITIAL_BLOCK);const { startBlock, endBlock, ...dragInfo } = useDragBlock({ handleSubmit });

  return (
    <Styled.Root id={scheduleInfo?._id} {...dragInfo}>
      {timeArr.map((el: number) => (
        <TimeBlock
          id={el}
          key={el}
          isUsed={scheduleInfo.isCompleted || false}
          startBlock={startBlock}
          endBlock={endBlock}
          isDraged={isDraged(el)}
        />
      ))}
    </Styled.Root>
  );
  const [isDragging, setIsDragging] = useState(false);

  const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    if (e.target instanceof HTMLDivElement && e.target.id.length < 3) {
      setStartBlock(parseInt(e.target.id));
      setEndBlock(parseInt(e.target.id));
      setIsDragging(true);
    }
  };

  const onMouseOver = (e: React.MouseEvent<HTMLDivElement>) => {
    if (isDragging) {
      if (e.target instanceof HTMLDivElement && e.target.id.length < 3) {
        setEndBlock(parseInt(e.target.id));
      }
    }
  };

  const onMouseUp = () => {
    handleSubmit();
    setIsDragging(false);
    setStartBlock(INITIAL_BLOCK);
    setEndBlock(INITIAL_BLOCK);
  };

  return {
    onMouseDown,
    onMouseOver,
    onMouseUp,
    startBlock,
    endBlock,
  };
}

export default useDragBlock;

각각의 블럭이 모여 한 줄을 이루는 Blocks 컴포넌트에 해당 이벤트를 붙여 이벤트 위임을 통해서 이벤트를 감지하고 로직을 실행할 수 있게 하였다. useEffect를 이용하여, endBlock이 변경 될때마다 그 사이에 있는 블럭들의 background color을 변경하도록 하였다.

const { startBlock, endBlock, ...dragInfo } = useDragBlock({ handleSubmit });

  return (
    <Styled.Root id={scheduleInfo?._id} {...dragInfo}>
      {timeArr.map((el: number) => (
        <TimeBlock
          id={el}
          key={el}
          isUsed={scheduleInfo.isCompleted || false}
          startBlock={startBlock}
          endBlock={endBlock}
          isDraged={isDraged(el)}
        />
      ))}
    </Styled.Root>
  );
function TimeBlock(props: timeType) {
  const { id, isUsed, startBlock, endBlock, isDraged } = props;

  const [draged, setDraged] = useState(isDraged);

  useEffect(() => {
    if (startBlock <= id && id <= endBlock) {
      isUsed ? setDraged('done') : setDraged('plan');
    }
    if (startBlock >= id && id >= endBlock) {
      setDraged('');
    }
    if (startBlock === id && endBlock === id) {
      if (draged === '') {
        isUsed ? setDraged('done') : setDraged('plan');
      } else {
        setDraged('');
      }
    }
  }, [endBlock]);

  return <Styled.Block id={`${id}`} hourEnd={id % 4 === 3} draged={draged} />;
}

왜 useEffect를 이용해서 매번 endBlock이 바뀔때마다 실행되게 했을까?

일단.. 나도 이러고 싶지는 않았지만… 딱히 방법이 생각나지 않았다. 아주 수많은 삽질 끝에 선택한 방법.

처음에는 startBlock과 endBlock없이 단순히 mouse enter 이벤트가 발생하면, 그 블럭의 background color을 변경하는 로직을 작성하였다

하지만 여기서 큰 문제 발생!!!!!!!!!!!

💡 자바스크립트의 이벤트 감지는, 아주 미세한 시간의 간격을 두고 감지를 한다. 해당 감지 시간에 이벤트가 발생을 했다면 이벤트 함수가 실행되는 구조

그래서, 마우스를 빠르게 드래그를 했을때, 미세한 시간 간격 사이에 이벤트가 발생을 하게 되면 그 이벤트 발생을 감지하지 못하고 해당 컴포넌트를 뛰어 넘어버리는 것이다. 그래서 블럭 사이에 구멍이 숭숭 남..

이 문제는 자바스크립트 이벤트 감지의 특성이라 해결해보려 노력 했지만, 결국 endBlock 변수를 두고 useEffect를 사용하여 만약에 중간에서 이벤트를 감지하지 못했더라도 다음 endBlock을 감지하였을때 start와 end 사이의 블럭을 다 색칠하여 중간에 빈 블럭이 없게끔 해주었다.

이 방법이 완전한 해결방법은 아니라고 생각한다. 애초에 useEffect가 계속해서 실행되기에 좋지 못한 방식 같고 브라우저에 cost가 굉장한 작업 같다. 이 문제에 대해서 좀 더 고민하고 더 좋은 해결방법을 찾는 과정이 필요할 것 같다.

profile
FrontEnd Developer

0개의 댓글