react에서 drag and drop 기능을 가능하게 하는 라이브러리는 react-draggable
, react-dnd
, react-beautiful-dnd
로 총 3가지가 있다.
npm-trend 를 보면 알 수 있듯이 react-draggable이 제일 사용률이 높고, react-dnd와 react-beautiful-dnd는 비슷하게 사용되고 있다.
react-draggable
드래그로 어떠한 아이템 간의 순서를 변경하는 것보다 윈도우안에서 어떠한 아이템을 드래그해서 위치만을 바꾸는 부분에서 강점이 있는 라이브러리이다.
react-dnd
, react-beautiful-dnd
드래그로 어떠한 아이템 간의 순서를 변경하는 것에 강점이 있는 라이브러리이다.
여기서 react-dnd는 hover중일때 순서가 변경되는 애니메이션이나 위치 변경을 직접 정의해야 한다.
반면, react-beautiful-dnd는 UI/UX나 퍼포먼스가 좋은 동작이 미리 정의되어 있다. 그래서 react-dnd보다 용량이 약 2배 많다.
직접 커스터마이징 하기 전에 react안에서의 drag and drop의 기능 구현을 해보고 싶어서 이번에는 react-beautiful-dnd를 공부하기로 하였다.
React + TypeScript + react-beautiful-dnd
로 진행하였다.
// CRA 🖥
yarn create react-app RBD --template typescript
yarn add typescript @types/node @types/react @types/react-dom @types/jest
// rbd ✨
yarn add react-beautiful-dnd @types/react-beautiful-dnd
타입스크립트를 사용하기에 타입도 함께 설치해주어야 한다.
react를 strictMode로 동작시키는 경우, 아래와 같이 라이브러리가 제대로 작동하지 않는다.
따라서 index.tsx에 선언되어 있는 React.strictMode를 제거해주어야 한다.
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
// <React.StrictMode>
<App />
/* </React.StrictMode> */
);
strictMode를 제거하였기 떄문에, 렌더링 검증을 하는 부분이 필요하다고 생각하였다.
따라서 아이템리스트를 drag 가능하게 하는 컴포넌트를 렌더링 이후에
나타나게 하도록 검증하는 DragLand.tsx
를 App.tsx에서 불러와 사용하였다.
import DragLand from "../src/components/DragLand";
export default function App() {
return (
<>
<DragLand />
</>
);
}
import { useState, useEffect } from "react";
import Drag from "./Drag";
export default function DragLand() {
const [ready, setReady] = useState<Boolean>(false);
useEffect(() => {
setReady(true);
}, []);
return <div>{ready ? <Drag /> : null}</div>;
}
렌더링을 검사하는 ready 변수를 Boolean의 state로 만들고, 초기값으로 false를 주었다. 이후 컴포넌트 초기 렌더링 이후에 실행되는 useEffect
를 사용하여 ready의 값을 true로 변환시켰다.
결과적으로 렌더링이 정상적으로 완료된다면 Drag
컴포넌트가 불러와질 것이고, 그렇지 않다면 null값을 반환할 것이다.
Drag.tsx에서 react-beautiful-dnd를 사용하기 위한 코드는 아래와 같다.
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
} from "react-beautiful-dnd";
drag할 아이템 리스트들이 움직일 범위를 지정해주는 DragDropContext
컴포넌트를 제일 처음 지정해주어야 하고, 그 안에 Droppable
컴포넌트를 넣어 움직일 아이템의 리스트들의 범위를 생성한다.
<DragDropContext>
<Droppable droppableId="todos">
{(provided) => (
<ul
className="todos"
{...provided.droppableProps}
ref={provided.innerRef}
>
// ✨ 움직일 아이템 리스트들을 넣어주는 곳 ✨
</ul>
)}
</Droppable>
</DragDropContext>
Droppable의 droppableId === provided를 return하는 html element의 className
이 반드시 일치해야 한다.div태그
를 사용해야 한다. {(provided) => (
<div
className="todos"
{...provided.droppableProps}
ref={provided.innerRef}
>
// ✨ 움직일 아이템 리스트들을 넣어주는 곳 ✨
<TodoList>
</TodoList>
</div>
)}
<DragDropContext onDragEnd={handleChange}>
<Droppable droppableId="todos">
{(provided) => (
<ul
className="todos"
{...provided.droppableProps}
ref={provided.innerRef}
> 👇
{todos.map(({ id, title }, index) => (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
// ✨ 움직일 아이템 리스트들을 넣어주는 곳 ✨
</li>
)}
</Draggable>
))}
</ul>
)}
</Droppable>
</DragDropContext>
Draggable 의 draggableId === key
는 반드시 일치해야 한다.<Draggable key={id} draggableId={id} index={index}>
아이템 컴포넌트를 이동할 때, 원하는 자리에 올바르게 위치하도록 하기 위해서placeholder 함수를 추가한다.
<DragDropContext onDragEnd={handleChange}>
<Droppable droppableId="todos">
{(provided) => (
<ul
className="todos"
{...provided.droppableProps}
ref={provided.innerRef}
>
{todos.map(({ id, title }, index) => (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
// ✨ 움직일 아이템 리스트들을 넣어주는 곳 ✨
</li>
)}
</Draggable>
))}
👇
{ provided.placeholder }
</ul>
)}
</Droppable>
</DragDropContext>
만약 이를 사용하지 않으면, 정상적으로 작동하긴 하지만 placeholder가 없다는 에러 메세지가 뜬다.
따라서 이를 반드시 작성해주도록 하자.
onDragEnd 이벤트
로 사용할 mouse drag로 이동한 아이템을 drop하여 drag가 끝났을 때의 결과를 저장하는 함수를 작성해야 한다.
const handleChange = (result: DropResult) => {
if (!result.destination) return;
const items = [...todos];
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setTodos(items);
};
result.destination(결과)가 존재하지 않으면 그냥 반환하고
존재한다면 원본 리스트를 복사하여 splice메서드를 사용해
선택한 아이템을 가져와 이를 결과의 인덱스에 삽입시킨 복사 리스트로 state를 변경한다.
📚 학습할 때, 참고한 자료 📚