혼자 적어보는 노트

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

React

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

jinist 2022. 1. 6. 15:03

 

종종 사이트에서 드래그&드롭 이벤트가 보이길래
투두리스트를 작업하면서 나도 해당 기능을 적용해보기로 했다.

 

라이브러리를 사용해야 될 것만 같았는데
HTML 드래그앤 드롭 API가 존재 한다는 것을 알게되었다.

여러 블로그나 글을 읽고 해본 다음에 공식문서를 보는 것이 도움이 되었다.

 

+ 참고블로그

 


 

드래그 api를 사용할 때 필요한 것.

 

 

1. draggable 속성 추가.

드래그 api를 사용하려면 html 태그 안에 draggable이라는 속성을 넣어주어야 한다.

<li draggable></li>


2. 이벤트 지정.

사용할 이벤트를 지정해준다.
주요 이벤트 이름은 아래와 같다.

drag
드래그 할 때 발생


dragstart
드래그하기 시작했을 때 발생


dragend
드래그를 끝냈을 때 발생


dragenter
드래그한 요소가 드롭 대상 위에 올라갔을 때 발생.


dragleave
드래그하는 요소가 드롭대상에서 벗어났을 때 발생


dragover
드래그한 요소가 드롭대상 위로 지나갈 때 발생 (수백 밀리초 마다 발생)


drop
드래그한 요소가 드롭대상에 드롭했을 때 발생.

 

 

3. dataTransfer

모든 drag 이벤트에는 dataTransfer라는 드래그 데이터를 보유하고 있는데
무엇을 끌고 있는지 데이터의 유형을 설정 해 주어야 한다.

* 이것을 설정하면 드랍할 곳으로 데이터를 보내줄 수 있다.

event.dataTransfer.setData("text/html", event.target);

 

▼ dataTransfer 공식문서
https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types

 

 

예시)

  $todoList.addEventListener("dragstart", (e) => {
      e.dataTransfer.setData("id", e.target.id);
  });


  $todoList.addEventListener("drop", (e) => {
    console.log(e.dataTransfer.getData("id")); // e.target.id의 값이 나타난다
  });

 

4. dropEffect, effectAllowed

드래그 대상과 드롭되는 타깃이 어떤 동작을 수행할지 지정해야 한다.

 

dropEffect

드롭 이벤트에서 어떤 동작을 하는지 정할 수 있다.

* dragenter/dragover이벤트 핸들러에서 지정

 

none : 이곳에 드롭 불가

move : 드래그 대상을 드롭 타깃으로 옮김

copy : 드래그 대상을 드롭 타깃으로 복사

link : 드래그 대상이 URL이라면 드롭 타깃이 이동

 

effecAllowed

드래그 대상에 허용할 dropEffect를 정할 수 있다.

* dragstart이벤트 핸들러에서 지정.

 

uninitialized : 아무것도 지정안함
none : 아무것도 허용안함
copy : dropEffect copy만 허용
link : dropEffect link만 허용
move : dropEffect copy만 허용
copyLink : dropEffect copy, link 허용
copyMove : dropEffect copy, move만 허용
linkMove : dropEffect link, move만 허용
all : dropEffect값 모두 허용 / 기본값

 

5. 드롭 허용

드롭을 허용하려면 dragEnter, dragOver 이벤트를 취소하여 기본처리를 방지해야 한다.

  const onDragOver = (event) => {
    event.preventDefault();
  };

 

위의 세가지를 설정하여 테스트로 작업해보았다.

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 onDragOver = (event) => {
    event.preventDefault();
    console.log("DragOver");
  };
  const onDragStart = (event) => {
    event.dataTransfer.setData("text/html", event.target); //데이터 유형 설정
    console.log("DragStart");
  };
  const onDragEnd = (event) => {
    console.log("DragEnd");
  };
  const onDrop = (event) => {
    console.log("Drop");
  };
  const onDragLeave = (event) => {
    console.log("DragLeave");
  };
  const onDragEnter = (event) => {
    event.preventDefault();
    console.log("DragEnter");
  };

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

export default DragAndDrop;

 

 

 

dragStart - dragEnter - dragOver - dragLeave - drop - dragEnd

위 순서대로 콘솔에 나오는 것을 볼 수 있다.

 

 

const DragAndDrop = (props) => {
  const [lists, setLists] = useState(list);
  const [drag, setDrag] = useState(null); // 드래그를 하려는 요소를 담는 state

  const onDragOver = (event) => {
    event.preventDefault();
  };
  const onDragStart = (event) => {
    setDrag(event.target);  // 드래그 시작 시 데이터를 담는다
    event.dataTransfer.setData("text/html", event.target); // 데이터 속성 설정
  };

  const onDragEnter = (event) => {
    event.preventDefault();
    let dragIndex = Number(drag.dataset.index); // 드래그하는 요소의 원래 인덱스
    let targetIndex = Number(event.target.dataset.index); // 드래그후 지나가는 요소의 인덱스

    let _lists = [...lists];
    _lists[dragIndex] = _lists.splice(targetIndex, 1, _lists[dragIndex])[0];
    // 지나가는 요소의 인덱스와 드래그한 요소의 인덱스를 이용하여 위치변경
    setLists(_lists);
  };
  
    const onDragLeave = (event) => {
    if (event.target === drag) { 
      event.target.style.visibility = "hidden"; // 머무를 때 요소를 안보이게 처리
    }
  };

  const onDragEnd = (event) => {
    setDrag(null);
    event.target.style.visibility = "visible"; 
  };

  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}
          draggable
        >
          {list.name}
        </li>
      ))}
    </ul>
  );
};

 

 

 

드래그 후 요소를 지나갈 때 마다 위치를 서로 바꾸어 주었는데

위와 같이 작업 시 각 li에서 순서대로 지나가면 문제가 없지만 드래그후 ul의 영역 바깥으로 이동했다가

드롭을 하면 원하는 결과가 나오지 않는다.

 

곰곰히 생각해보면 당연한 결과겠지만,

1번드래그 -> ul바깥쪽 이동 -> 다른요소들을 거치지않고 5번으로 바로 이동 시 1번과 5번이 뒤바뀜.

 

 

문제 해결

DragEnter코드 수정.

  const onDragEnter = (event) => {
    let dragIndex = Number(drag.dataset.index);
    let targetIndex = Number(event.target.dataset.index);

    let _lists = [...lists];
    let _listItem = _lists[dragIndex];
    if (dragIndex !== targetIndex) {
      _lists.splice(dragIndex, 1);
      _lists.splice(targetIndex, 0, _listItem);
      setLists(_lists);
    }
  };

단순 위치만 바꾸어주는 코드가 아닌 드래그하는 요소와 드래그한 후에 닿게되는 요소의 index를 이용하여

드래그하고있는 index의 요소를 삭제해주고 드래그해서 닿는 위치에 추가를 해주는 식으로 변경하였다.

 

 

 

전체코드

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 onDragOver = (event) => {
    event.preventDefault();
    return false;
  };
  const onDragStart = (event) => {
    setDrag(event.target);
    event.dataTransfer.effectAllowed = "move";
    event.dataTransfer.setData("text/html", event.target);
  };
  const onDragEnd = (event) => {
    setDrag(null);
    event.target.style.visibility = "visible";
  };

  const onDragLeave = (event) => {
    if (event.target === drag) {
      event.target.style.visibility = "hidden";
    }
  };
  const onDragEnter = (event) => {
    let dragIndex = Number(drag.dataset.index);
    let targetIndex = Number(event.target.dataset.index);

    let _lists = [...lists];
    let _listItem = _lists[dragIndex];
    if (dragIndex !== targetIndex) {
      _lists.splice(dragIndex, 1);
      _lists.splice(targetIndex, 0, _listItem);
      setLists(_lists);
    }
  };

  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}
          draggable
        >
          {list.name}
        </li>
      ))}
    </ul>
  );
};

 

코드에 비해..

꽤 오랜시간 씨름했지만 그래도 나름 원하던 결과가 나왔다.

 

 

 

 

Comments