[React] Drag 구현하기

hhnn0·2022년 12월 20일
2
post-thumbnail

🧾 개요

에디터 개발 프로젝트를 진행하면서, 드래그 및 컴포넌트 선택 기능을 구현해보았습니다.


프로젝트 주소
https://github.com/boostcampwm-2022/web32-bmNotion


🖱 시작에 앞서

기본 동작

  • 드래그를 통해 블록들을 선택할 수 있습니다.
  • 드래그 대신 클릭을 할 경우 선택한 블록들이 해제됩니다.

이벤트

  • onMouseDown
    마우스 버튼이 눌려질 때 작동.

  • onMouseMove
    마우스 포인터가 컴포넌트 위에서 움직일때 작동.

  • onMouseUp
    눌렸던 마우스 버튼이 올라올 때 작동.

컴포넌트

  • Wrapper
    전체 컴포넌트들을 감싸는 컴포넌트

  • MainContainerBody
    드래그를 시작할 수 있는 컴포넌트

  • DragRange
    드래그 컴포넌트

  • BlockContentBox
    블록

State

  • mouseStartPosition
    마우스 클릭이 시작되는 위치.
  • mousePosition
    실시간으로 움직이는 마우스의 위치.

드래그 컴포넌트 구현

onMouseDown

마우스 클릭 위치를 저장한다.

// MainContainerBody component
 onMouseDown={(e) => {
	setMouseStartPosition({
		...mouseStartPosition,
		positionX: e.pageX,
        positionY: e.pageY,
	});
}}

onMouseMove

마우스가 클릭 된 상태라면 현재 마우스 커서의 위치를 저장한다.

// Wrapper component
onMouseMove={(e) => {
    if (mouseStartPosition.positionX && mouseStartPosition.positionY) {
    	setMousePosition({ ...mousePosition, positionX: e.pageX, positionY: e.pageY });
    }
}}

onMouseUp

마우스 버튼이 올라오면서 마우스 위치와 관련된 모든 state를 초기화한다.

// Wrapper component
onMouseUp={(e) => {
        setMouseStartPosition({ ...mouseStartPosition, positionX: null, positionY: null });
    	setMousePosition({ ...mousePosition, positionX: null, positionY: null });
}}

DragRange

props로 구현했을 때 마우스의 위치가 바뀔 때마다 컴포넌트가 재생성되는 문제가 발생하여
in-line style로 수정하였습니다.

🤕 props로 구현

  • component
<DragRange
    startPositionX={mouseStartPosition.positionX}
    startPositionY={mouseStartPosition.positionY}
    positionX={mousePosition.positionX}
    positionY={mousePosition.positionY}
/>
  • style
interface DragRangeProps {
  startPositionX: number | null;
  startPositionY: number | null;
  positionX: number | null;
  positionY: number | null;
}

const DragRange = styled.div<DragRangeProps>`
  background-color: rgba(35, 131, 226, 0.15);
  width: 100%;
  height: 100%;
  position: absolute;
  left: ${(props) => {
    if (props.startPositionX && props.positionX) {
      return Math.min(props.startPositionX, props.positionX).toString() + 'px';
    }
    return null;
  }};
  top: ${(props) => {
    if (props.startPositionY && props.positionY) {
      return Math.min(props.startPositionY, props.positionY).toString() + 'px';
    }
    return null;
  }};
  width: ${(props) => {
    if (props.positionX && props.startPositionX) {
      return (
        Math.abs(
          props.positionX - props.startPositionX
        )?.toString() + 'px'
      );
    }
    return 0;
  }};
  height: ${(props) => {
    if (props.positionY && props.startPositionY) {
      return (
        Math.abs(
          props.positionY - props.startPositionY
        )?.toString() + 'px'
      );
    }
    return 0;
  }};
`;


💊 마우스의 위치가 바뀔 때마다 컴포넌트가 재생성되는 문제 발생
(class name이 바뀐다 = 컴포넌트가 재생성된다)

😝 in-line style로 구현

  • Component
<DragRange
    style={{
    	left:
        	mouseStartPosition.positionX && mousePosition.positionX
            ? Math.min(mouseStartPosition.positionX, mousePosition.positionX).toString() + 'px'
            : '0px',
        top:
        	mouseStartPosition.positionY && mousePosition.positionY
            ? Math.min(mouseStartPosition.positionY, mousePosition.positionY).toString() + 'px'
            : '0px',
        width:
        	mouseStartPosition.positionX && mousePosition.positionX
            ? Math.abs(
                  mouseStartPosition.positionX - mousePosition.positionX
                ).toString() + 'px'
            : '0px',
        height:
        	mouseStartPosition.positionY && mousePosition.positionY
            ? Math.abs(
                  mouseStartPosition.positionY - mousePosition.positionY
                ).toString() + 'px'
            : '0px',
    }}
/>
  • style
const DragRange = styled.div`
  background-color: rgba(35, 131, 226, 0.15);
  position: absolute;
`;


블록 선택 구현

선택된 블록들에 selected 클래스를 추가하고,
추가 스타일을 적용해준다.

const BlockContentBox = styled.div<DraggableProps>`
.
.
.

  &.selected {
    background-color: rgba(35, 131, 226, 0.15);
  }
`;

드래그 범위 안의 블록 선택하기

블록의 선택과 해제가 실시간으로 이루어져야 하기 때문에 onMouseMove 이벤트에 추가해준다.
블록이 드래그 컴포넌트의 영역 안일 경우 selected class를 추가해주고, 영역 밖일 경우 selected class를 제거해준다.

// Wrapper component
onMouseMove={(e) => {
  if (mouseStartPosition.positionX && mouseStartPosition.positionY) {
    if (
      mouseStartPosition.positionX &&
      mouseStartPosition.positionY &&
      mousePosition.positionX &&
      mousePosition.positionY
    ) {
      const left = Math.min(mouseStartPosition.positionX, mousePosition.positionX);
      const top = Math.min(mouseStartPosition.positionY, mousePosition.positionY);
      const width = Math.abs(
        mouseStartPosition.positionX - mousePosition.positionX
      );
      const height = Math.abs(
        mouseStartPosition.positionY - mousePosition.positionY
      );
      const right = left + width;
      const bottom = top + height;
      blocks.forEach((block, i) => {
        const boxTop = // 블록 컴포넌트의 위
        const boxBottom = // 블록 컴포넌트의 아래
        const boxLeft = // 블록 컴포넌트의 왼쪽
        const boxRight = // 블록 컴포넌트의 오른쪽
        if (top <= boxBottom && boxTop <= bottom && left <= boxRight && boxLeft <= right) {
          block.classList.add('selected');
        } else {
          block.classList.remove('selected');
        }
      });
    }
  }
}}

선택된 블록 해제

마우스 클릭시 블록 선택을 초기화해준다.

onMouseDown={(e) => {
  blocks.forEach((e) => e.classList.remove('selected'));
}}

결과

0개의 댓글