TIL 111 - Drag And Drop In React

김영현·2024년 7월 24일
0

TIL

목록 보기
121/129

Drag And Drop

말 그대로 드래그 후 놓는 행위. 보통 파일을 끌어다 놓거나, 특정 요소의 위치를 옮길때 주로 사용한다.
윈도우에서는 아주 흔하게 사용하는 기능 중 하나다.

마음에 드는 인터랙티브 스킬 중 하나였는데, 지금까지 구현해 본적이 없었다.
이번 기회에 간단히라도 드래그 앤 드롭을 한번 구현해보자!
참고로 바닐라 JS가 아닌 React환경에서 구현할 것이다.


Drag And Drop을 위한 이벤트

드래그 앤 드롭을 위한 이벤트 종류는 꽤 많다.


출처 : https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API

  • drop : 드래그중인 아이템
  • dragend : 드래그 최종 완료.
  • dragenter : 드래그 중인 아이템이 엘리먼트에 닿았을 때 (한 번)
  • dragleave : 드래그 중인 아이템이 엘리먼트에서 벗어났을 때 (한 번)
  • dragover : 드래그 중인 아이템이 엘리먼트에 닿아 있을 때(수백 밀리초간격으로 체크함)
  • dragstart : 드래그를 시작 했을 때
  • drop : 드래그 중인 아이템을 유효한 drop target에 놓았을 때

이번에는 정말 간단히 구현해볼 생각이기에 3가지 이벤트만 사용해보겠음!

Simple React Drag and Drop

엘리먼트를 마우스로 드래그하여 다른 엘리먼트에 놓을 시, 놓은 위치에 있던 엘리먼트와 위치를 바꾸는 게 목표다.

먼저 엘리먼트를 드래그 가능하게 만들기 위하여 draggable옵션을 준다.

<h2 draggable>Drag and Drop List</h2>

이 옵션을 주면 위처럼 드래그가 가능하다!

다음은 React에서 엘리먼트의 위치를 어떻게 바꿀것인지 생각해보자.
React는 상태기반 렌더링이다. 따라서 각 엘리먼트를 상태 기반으로 렌더링 하겠다.

const initialItems = ["Item 1", "Item 2", "Item 3", "Item 4"];
const [items, setItems] = useState(initialItems);

...중략

{items.map((item, index) => (
          <li
            key={`${item},${index}`}
            draggable
          >
            {item}
          </li>
        ))}

이때 key프로퍼티는 유일해야한다. 위치를 바꾸는 작업을 해야하기에, 할당하지 않거나 인덱스로 할당 시 원치않는 순서가 될 수 있으므로 아이템과 인덱스를 더한 값을 사용했다.

이제 위치를 바꿔 볼 차례다. 각 엘리먼트의 위치는 상태 배열의 순서로 관리된다.
따라서 상태 배열 내 순서를 변경하면 엘리먼트의 위치가 바뀔 것이다.

현재 드래그중인 아이템의 인덱스를 참조하기위한 변수를 하나 선언한다. 이 값은 엘리먼트 자체를 렌더링 하기 위한 값이 아니기에, 상태가 아닌 useRef를 이용하여 관리한다.

//0은 인덱스기에, 초기값을 -1로 놓았다. findIndex등의 메서드에서 흔히 사용하는 방식!
const draggingIndex = useRef(-1);

드래그를 시작하면 드래그를 시작한 아이템의 인덱스를 저장한다.
이후 드래그 중인 아이템이 다른 엘리먼트에 닿아있을 때, 다른 엘리먼트의 인덱스를 이용하여 위치를 변경한다.

아래는 전체 코드다

import { useRef, useState } from "react";
const initialItems = ["Item 1", "Item 2", "Item 3", "Item 4"];

//배열 엘리먼트 자리변경을 위한 유틸함수
const swap = <T,>(arr: T[], index1: number, index2: number) =>
  ([arr[index1], arr[index2]] = [arr[index2], arr[index1]]);

const DragAndDrop = () => {
  const [items, setItems] = useState(initialItems);
  const draggingIndex = useRef(-1);

  //드래그 시작시 인덱스 저장
  const onDragStart = (index: number) => {
    draggingIndex.current = index;
  };

  //아래 이벤트 핸들러가 부착된 엘리먼트에 드래그중인 아이템이 닿으면 아래 이벤트 핸들러 발생
  const onDragOver = (index: number) => {
    if (draggingIndex.current === -1 || draggingIndex.current === index) return;

    const newItems = [...items];

    swap<string>(newItems, draggingIndex.current, index);
    draggingIndex.current = index;
    setItems(newItems);
  };

//드래그 이벤트 종료시 인덱스 초기화
  const onDragEnd = () => {
    draggingIndex.current = -1;
  };

  return (
    <div>
      <h2 draggable>Drag and Drop List</h2>
      <ul className="flex bg-emerald-200 w-50 h-10 p-2 gap-3">
        {items.map((item, index) => (
          <li
            className="h-full flex-1 flex justify-center items-center bg-indigo-300 cursor-pointer"
            key={`${item},${index}`}
            draggable
            onDragStart={() => onDragStart(index)}
            onDragOver={() => onDragOver(index)}
            onDragEnd={onDragEnd}
          >
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default DragAndDrop;

결과

간단한 드래그앤 드롭을 완성했다.


후기

생각보다 아주 간단한 작업이지만, 유저 경험을 상승시키는 인터랙티브한UI를 만드려면 복잡해질 수 있는 가능성이 충분하다. 파일 업로드도 한 번 다뤄보려했으나 토이프로젝트에 서버가 없어서 어떻게 연습해볼지 고민중이다!

profile
모르는 것을 모른다고 하기

0개의 댓글