말 그대로 드래그 후 놓는 행위. 보통 파일을 끌어다 놓거나, 특정 요소의 위치를 옮길때 주로 사용한다.
윈도우에서는 아주 흔하게 사용하는 기능 중 하나다.
마음에 드는 인터랙티브 스킬 중 하나였는데, 지금까지 구현해 본적이 없었다.
이번 기회에 간단히라도 드래그 앤 드롭을 한번 구현해보자!
참고로 바닐라 JS가 아닌 React환경에서 구현할 것이다.
드래그 앤 드롭을 위한 이벤트 종류는 꽤 많다.
출처 : https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
drop
: 드래그중인 아이템dragend
: 드래그 최종 완료.dragenter
: 드래그 중인 아이템이 엘리먼트에 닿았을 때 (한 번)dragleave
: 드래그 중인 아이템이 엘리먼트에서 벗어났을 때 (한 번)dragover
: 드래그 중인 아이템이 엘리먼트에 닿아 있을 때(수백 밀리초간격으로 체크함)dragstart
: 드래그를 시작 했을 때drop
: 드래그 중인 아이템을 유효한 drop target에 놓았을 때이번에는 정말 간단히 구현해볼 생각이기에 3가지 이벤트만 사용해보겠음!
엘리먼트를 마우스로 드래그하여 다른 엘리먼트에 놓을 시, 놓은 위치에 있던 엘리먼트와 위치를 바꾸는 게 목표다.
먼저 엘리먼트를 드래그 가능하게 만들기 위하여 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를 만드려면 복잡해질 수 있는 가능성이 충분하다. 파일 업로드도 한 번 다뤄보려했으나 토이프로젝트에 서버가 없어서 어떻게 연습해볼지 고민중이다!