혼자 적어보는 노트

[React] Drag and Drop 이벤트 응용해보기 / 문제해결 본문

React

[React] Drag and Drop 이벤트 응용해보기 / 문제해결

jinist 2022. 1. 8. 02:37

https://jinist.tistory.com/92?category=930695 

 

[React] Drag and Drop 이벤트 적용해보기

종종 사이트에서 드래그&드롭 이벤트가 보이길래 투두리스트를 작업하면서 나도 해당 기능을 적용해보기로 했다. 라이브러리를 사용해야 될 것만 같았는데 HTML 드래그앤 드롭 API가

jinist.tistory.com

 

이전에 시도했던 Drag and Drop이벤트.

맛보기로 감을 잡았으니 내 프로젝트에 적용할 응용버전을 만들어 보기로 했다.

 

하고싶은 기능의 구현사항

1. 리스트 드래그 시 클릭한 리스트 불투명처리

2. 리스트 드래그 시 들어갈 위치에 border 표시

3. 드롭 시 해당 위치에 해당 리스트 삽입

 

 

 

import React from "react";
import { useState } from "react/cjs/react.development";

const list = [
  { id: 1, name: "1. 할 일1" },
  { id: 2, name: "2. 할 일2" },
  { id: 3, name: "3. 할 일3" },
  { id: 4, name: "4. 할 일4" },
  { id: 5, name: "5. 할 일5" },
];

const DragAndDrop = (props) => {
  const [lists, setLists] = useState(list);
  const [drag, setDrag] = useState(null);
  const [clickTodo, setClickTodo] = useState(null);

  const onDragOver = (event) => {
    event.preventDefault();
    return false;
  };
  const onDragStart = (event) => {
    setDrag(event.target);
    event.target.style.opacity = "0.3";
    event.dataTransfer.effectAllowed = "move";
    event.dataTransfer.setData("text/html", event.target);
  };
  const onDragEnd = (event) => {
    setDrag(null);
    event.target.style.opacity = "1";
  };

  const onDragLeave = (event) => {
    event.target.classList.remove("bottom");
  };
  const onDragEnter = (event) => {
    console.log(event.target);
    event.target.classList.add("bottom");
  };
  const onDrop = (event) => {
    let dragIndex = Number(drag.dataset.index);
    let targetIndex = Number(event.target.dataset.index);
    console.log(dragIndex, targetIndex);

    let _lists = [...lists];
    let _listItem = _lists[dragIndex];
    if (dragIndex !== targetIndex) {
      _lists.splice(dragIndex, 1);
      _lists.splice(targetIndex, 0, _listItem);
      setLists(_lists);
    }
    event.target.classList.remove("bottom");
  };

  const onClick = (event) => {
    if (clickTodo) {
      clickTodo.classList.remove("selected");
      event.target.classList.add("selected");
    } else {
      event.target.classList.add("selected");
    }
    setClickTodo(event.target);
  };

  return (
    <ul className="drag">
      {lists.map((list, index) => (
        <li
          key={list.id}
          data-index={index}
          onDragStart={onDragStart}
          onDragEnd={onDragEnd}
          onDragEnter={onDragEnter}
          onDragLeave={onDragLeave}
          onDragOver={onDragOver}
          onDrop={onDrop}
          onMouseDown={onClick}
          draggable
        >
          {list.name}
        </li>
      ))}
    </ul>
  );
};

export default DragAndDrop;

 

처음엔 위와 같이 코드를 짰는데 border을 bottom에만 주다 보니

첫번 째 li를 밑으로 내리는 것에 대해서는 문제가 없었으나

중간위치나 맨 밑의 위치에서 리스트를 위로 올리는 부분에서 이상함이 느껴졌다.

 

 

  const onDragEnter = (event) => {
    let dragIndex = Number(drag.dataset.index);
    let targetIndex = Number(event.target.dataset.index);
    if (dragIndex < targetIndex) {
      event.target.classList.add("bottom");
    } else {
      event.target.classList.add("top");
    }
  };

DragEnter에서 dragIndex와 targetIndex를 비교해서 border의 위치를 변경해주는 것으로 해결하였다.

 

 

 

 

처음에 생각한 대로 동작되었다.

간단하게 만들어 보고 이제 미니 프로젝트에 적용해보려고 하는데

여러가지의 문제가 발생했다.

 

 

1.

위의 테스트 작업에서는 li 안에 다른 HTML태그가 없었다.

하지만 적용하려는 프로젝트에서는 li안에 아이콘이나, 체크박스 등 다른 html태그를 넣어 놓았고

li를 드래그하면 dropEnter에서 event.target이 해당 하위요소들을 가르킨다는 것이다..

event.target을 우회하는 방법이나 이벤트를 막는 방법들을 찾아보았는데

pointer-events: none;을 하기에는 li안에 체크박스이벤트와

삭제버튼 클릭 이벤트가 있어서 막을 수 없었다.

그렇다고 dragStart 시에만 해당 이벤트에 pointer-events 속성을 넣는 것은 효율적이지 못할 것 같았다.

 

=> 문제해결: event.currentTarget속성을 발견했다.

이 속성을 이용하면 자식요소가 아닌 이벤트를 발생시킨 target을 받을 수 있다.

 

 

2. 

위의 테스트 작업에서는 한 컴포넌트안에서 코드를 모두 구현했었지만,

이번에는 ul 이 담긴 컴포넌트<Todo />와 map을 돌려서 li를 출력하는 컴포넌트<TodoItem />로 나누어서 작업했다.

drag정보를 담는 drag state를 <TodoItem />에서 만들었는데

드래그를하고 다음 li로 마우스를 옮기면 dragEnter에서 drag 정보를 얻을 수 없는 것이다.

 

=> 문제해결: <TodoItem />를 감싸는 <Todo />컴포넌트에서 state를 생성하여 전달.

map을 돌린 컴포넌트 안에서 state를 생성하면 갯수만큼 state가 돌아간다. 당연한 원리였다.

 

 

3. 

currentTartget으로 dragEnter, drop에서 li의 자식 요소가 선택되는것은 막았지만, (1번)

dragLeave를 마주칠 시 border효과를 해제하는 코드를 담았는데, 

li를 드래그 한 상태에서 자식 요소에 커서가 닿을 경우

currentTarget은 li일 뿐이고, 해당 li를 벗어난다고 판단해서 그런지

dragLeave가 발생되어서 border가 지워지는 현상 발생.

 

=> 문제해결: dragEnter에서 current에 데이터를 담아준 후 데이터가 같지 않을 경우에만 실행.

  const onDragLeave = (event) => {
    if (event.currentTarget !== current) {
      event.currentTarget.classList.remove("bottom");
      event.currentTarget.classList.remove("top");
    }
  };

 

4.

문제들을 전부 해결하고 뒤늦게 발견한 부분인데

dragEnter시 drag하는 요소 기준으로 currentTarget의 인덱스가 높으면 (1 -> 5의 위치로이동)

border bottom을, 인덱스가 낮으면 (5 -> 1의 위치로 이동) border top을 주었는데

생각해보니 이건 인덱스기준이 아니라 내 드래그 커서 위치 기준으로 옮겨지는게 맞는 것 같다.

 

==> 문제해결: 드래그 중인 커서의 위치를 offsetHeight와 offsetY를 이용하여 계산한 후

drop시에 드래그중인 요소의 위치를 변경할 기준선(border)를 표시하고 drop시 그 위치에 맞게 바뀌도록 한다.

 

  let current = null;
  let height = 0;
  let moveY = 0;
  let movePosition = height / 2;

  const onDragOver = (event) => {
    event.preventDefault();
    height = event.target.offsetHeight; // li의 height
    moveY = event.nativeEvent.offsetY; // li기준으로 드래그 커서 위치
    movePosition = height / 2; // 위,아래의 기준이 될 절반의 값

    if (movePosition > moveY) {
      event.currentTarget.classList.add("top");
      event.currentTarget.classList.remove("bottom");
    } else {
      event.currentTarget.classList.add("bottom");
      event.currentTarget.classList.remove("top");
    }
    return false;
  };
  

  const onDrop = (event) => {
    let dragIndex = Number(drag.dataset.index);
    let targetIndex = Number(event.currentTarget.dataset.index);
    let _lists = [...todos];
    let _listItem = _lists[dragIndex];

    event.currentTarget.classList.remove("bottom");
    event.currentTarget.classList.remove("top");

    if (dragIndex == targetIndex) {
      return;
    }

    if (dragIndex < targetIndex) {
      if (movePosition > moveY) {
        _lists.splice(dragIndex, 1);
        _lists.splice(targetIndex - 1, 0, _listItem);
      } else {
        _lists.splice(dragIndex, 1);
        _lists.splice(targetIndex, 0, _listItem);
      }
    }
    if (dragIndex > targetIndex) {
      if (movePosition > moveY) {
        _lists.splice(dragIndex, 1);
        _lists.splice(targetIndex, 0, _listItem);
      } else {
        _lists.splice(dragIndex, 1);
        _lists.splice(targetIndex + 1, 0, _listItem);
      }
    }
    dispatch(updateTodo(_lists));
  };

 

 

 

중간중간의 문제들이 있어서 꽤 오랜시간 고생했지만 포기하지 않기를 잘한 것 같다

작동이 잘 안되서 여러가지를 사용해보고 실패하고 왜 안될까 생각을 많이 해본 것 같다.

결국은 딱 원하는대로 전부 구현되서 마음에 들었다. 코드는 좀 더 정리해 볼 예정이다.

다음 기능을 구현해야하기 때문에 바로 다른 드래그 예제를 해보진 못하겠지만

다음번에는 애니메이션이 함께 들어간 것도 만들어보고싶다.

drag api가 아닌 mouse이벤트로!

 

 

Comments