사고쳤어요
[React / ts] Task App 만들기 - ⑥ dnd-kit으로 drag and drop 만들기 본문
https://www.npmjs.com/package/@dnd-kit/sortable
@dnd-kit/sortable
Official sortable preset and sensors for dnd kit. Latest version: 10.0.0, last published: 4 months ago. Start using @dnd-kit/sortable in your project by running `npm i @dnd-kit/sortable`. There are 1119 other projects in the npm registry using @dnd-kit/sor
www.npmjs.com
dnd-kit은 drag and drop을 구현하는 데 도움을 주는 라이브러리이다.
npm install @dnd-kit/sortable
다음 명령어로 설치를 진행한 뒤 본격적으로 구현을 시작해보자.
Task drag and drop
// List.tsx
import React from "react";
import { GrSubtract } from "react-icons/gr";
import { IList, ITask } from "../../types";
import { useTypedDispatch, useTypedSelector } from "../../hooks/redux";
import {
deleteList,
setModalActive,
updateBoards,
} from "../../store/slices/boardsSlice";
import { addLog } from "../../store/slices/loggerSlice";
import { v4 } from "uuid";
import { setModalData } from "../../store/slices/modalSlice";
import Task from "../Task/Task";
import { deleteButton, header, listWrapper, name } from "./List.css";
import ActionButton from "../ActionButton/ActionButton";
import {
DndContext,
closestCenter,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
type TListProps = {
list: IList;
boardId: string;
};
type TSortableItem = {
task: ITask;
boardId: string;
id: string;
index: number;
};
const SortableItem: React.FC<TSortableItem> = ({
task,
boardId,
id,
index,
}) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
marginBottom: "8px",
cursor: "grab",
};
return (
<div style={style} ref={setNodeRef} {...attributes} {...listeners}>
<Task
taskName={task.taskName}
taskDescription={task.taskDescription}
boardId={boardId}
id={task.taskId}
index={index}
/>
</div>
);
};
const List: React.FC<TListProps> = ({ list, boardId }) => {
const sensors = useSensors(useSensor(PointerSensor));
const boards = useTypedSelector((state) => state.boards.boardArray);
const dispatch = useTypedDispatch();
const handleListDelete = (listId: string) => {
dispatch(deleteList({ boardId, listId }));
dispatch(
addLog({
logId: v4(),
logMessage: "리스트 삭제하기",
logAuthor: "User",
logTimestamp: Date.now().toString(),
})
);
};
const handleTaskChange = (
boardId: string,
listId: string,
taskId: string,
task: ITask
) => {
dispatch(setModalData({ boardId, listId, task }));
dispatch(setModalActive(true));
};
const findTaskLocation = (taskId: string | number) => {
for (const board of boards) {
for (const list of board.lists) {
const found = list.tasks.find((t) => t.taskId === taskId.toString());
if (found) {
return {
boardId: board.boardId,
listId: list.listId,
taskId: found.taskId,
};
}
}
}
return null;
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const source = findTaskLocation(active.id);
const destination = findTaskLocation(over.id);
if (source?.listId === destination?.listId && source && destination) {
const targetBoardIndex = boards.findIndex(
(b) => b.boardId === source.boardId
);
const targetListIndex = boards[targetBoardIndex].lists.findIndex(
(l) => l.listId === source.listId
);
const newBoards = JSON.parse(JSON.stringify(boards));
const targetList = newBoards[targetBoardIndex].lists[targetListIndex];
const oldIndex = targetList.tasks.findIndex(
(t: ITask) => t.taskId === active.id
);
const newIndex = targetList.tasks.findIndex(
(t: ITask) => t.taskId === over.id
);
targetList.tasks = arrayMove(targetList.tasks, oldIndex, newIndex);
dispatch(updateBoards({ boards: newBoards }));
}
};
return (
<div className={listWrapper}>
<div className={header}>
<div className={name}>{list.listName}</div>
<GrSubtract
className={deleteButton}
onClick={() => handleListDelete(list.listId)}
/>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={list.tasks.map((task) => task.taskId)}
strategy={verticalListSortingStrategy}
>
{list.tasks.map((task, index) => (
<div
key={task.taskId}
onClick={() =>
handleTaskChange(boardId, list.listId, task.taskId, task)
}
>
<SortableItem
key={index}
task={task}
boardId={boardId}
id={task.taskId}
index={index}
></SortableItem>
</div>
))}
</SortableContext>
</DndContext>
<ActionButton boardId={boardId} listId={list.listId} />
</div>
);
};
export default List;
dnd-kit을 사용하여 드래그 앤 드롭을 구현하려면 3가지 컴포넌트가 제 역할을 수행해야 한다.
<DndContext>: 드래그 앤 드롭을 사용하는 영역
<SortableContext>: 드래그한 아이템을 놓을 수 있는 영역
<SortableItem>: 드래그할 아이템
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={list.tasks.map((task) => task.taskId)}
strategy={verticalListSortingStrategy}
>
{list.tasks.map((task, index) => (
<div
key={task.taskId}
onClick={() =>
handleTaskChange(boardId, list.listId, task.taskId, task)
}
>
<SortableItem
key={index}
task={task}
boardId={boardId}
id={task.taskId}
index={index}
></SortableItem>
</div>
))}
</SortableContext>
</DndContext>
위 부분에 해당되는 코드는 다음과 같다.
sensor, collisionDetection 등은 dnd-kit에서 제공하는 모듈로 간편하게 설정이 가능하다.
우리가 집중해서 구현해주어야 할 부분은 handleDragEnd로, 드래그가 끝났을 때를 처리하는 함수이다.
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const source = findTaskLocation(active.id);
const destination = findTaskLocation(over.id);
if (source?.listId === destination?.listId && source && destination) {
const targetBoardIndex = boards.findIndex(
(b) => b.boardId === source.boardId
);
const targetListIndex = boards[targetBoardIndex].lists.findIndex(
(l) => l.listId === source.listId
);
const newBoards = JSON.parse(JSON.stringify(boards));
const targetList = newBoards[targetBoardIndex].lists[targetListIndex];
const oldIndex = targetList.tasks.findIndex(
(t: ITask) => t.taskId === active.id
);
const newIndex = targetList.tasks.findIndex(
(t: ITask) => t.taskId === over.id
);
targetList.tasks = arrayMove(targetList.tasks, oldIndex, newIndex);
dispatch(updateBoards({ boards: newBoards }));
}
};
active는 드래그를 시작한 아이템, 즉 옮길 아이템을 뜻하고 over는 드래그를 마친 장소의 아이템을 뜻한다.
즉 active를 클릭하여 over로 드래그하면 active와 over 두 개의 위치가 바뀌는 것이다.
findTaskLocation()함수를 정의해주어 task가 위치한 Board와 List의 위치를 찾아준 뒤,
active와 over의 taskId를 통해 두 위치를 변경하여 최종적으로 dispatch(updateBoards())를 통해 redux에 반영해주었다.
'웹 풀스택' 카테고리의 다른 글
[React / ts] Book Store 만들기 - ① 프로젝트 생성과 폴더 구조 설정 (0) | 2025.04.17 |
---|---|
[React / ts] Task App 만들기 - ⑦ Firebase 연동하여 로그인, 로그아웃 만들기 (0) | 2025.04.17 |
[React / ts] Task App 만들기 - ⑤ Logger Modal 만들기 (0) | 2025.04.17 |
[React / ts] Task App 만들기 - ④ Modal 만들기 (0) | 2025.04.17 |
[React / ts] Task App 만들기 - ③ ActionButton 만들기 (0) | 2025.04.15 |