[React] DragAndDrop(DND) 사용하기 (Typescript)
React DND
- 참고사이트
DND 란?
간단하다. 드래그와 드랍을 통해 특정 창의 위치를 변경한다는 의미이다.
React 환경에서 DND 기능을 제공해주는 모듈은 대표적으로 3개가 존재한다.
- react-draggable
드래그로 아이템 간의 순서를 변경하는 기능보다, 아이템의 위치를 옮기능 기능에 강점이 있다.
- react-dnd
DND 기능을 사용 할 수 있게 해주는 hook 을 제공해주는 모듈 이다.
- react-beautiful-dnd
react-dnd 에서 좀 더 확장된 느낌으로 UI/UX 퍼포먼스가 좋은 동작이 미리 정의되어 있다. 이처럼 편리한 기능을 제공하기 때문에
react-dnd 보다 용량이 약 2배 많다.
학습 후 우리가 얻게 될 결과물은 아래와 같다.
라이브러리 다운로드
1
npm i react-dnd react-dnd-html5-backend
프로젝트 구조
1
2
3
4
5
6
7
8
9
10
11
src
┣ constants
┃ ┣ dnd.ts
┣ components
┃ ┣ draganddrop
┃ ┣ Column.tsx
┃ ┣ DraggableComponent.tsx
┃ ┣ MovableItem.tsx
┃ ┣ tasks.ts
┣ types
┃ ┣ DragAndDrop.ts
CSS 추가
일단 각 컴포넌트에서 사용 될 css 를 추가하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.dnd-container {
display: flex;
flex-direction: row;
justify-content: space-around;
}
.dnd-column {
height: 600px;
width: 200px;
margin: 20px;
border-radius: 10px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5);
justify-content: center;
flex-wrap: wrap;
}
.dnd-first-column {
background-color: #f5ffea;
}
.dnd-second-column {
background-color: #fffbf5;
}
.dnd-movable-item {
border-radius: 10px;
background-color: #fff3f3;
height: 100px;
width: 170px;
margin: 10px auto;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
}
DraggableComponent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { useState } from 'react';
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { tasks } from "components/draganddrop/tasks";
import { COLUMN_NAMES, ITEM_TYPE } from "constants/dnd";
import MovableItem from "components/draganddrop/MovableItem";
import Column from "components/draganddrop/Column";
const DraggableComponent: React.FC = () => {
const [items, setItems] = useState(tasks);
const moveCardHandler = (dragIndex: number, hoverIndex: number) => {
const dragItem = items[dragIndex];
// 1. 사용자가 item을 드래그하고 있다면
if (dragItem) {
setItems((prevState: any) => {
// 2. 기존의 데이터(prevState)를 새로운 변수에 복사한다.
const coppiedStateArray = [...prevState];
// 3. splice로 hoverIndex 위치부터 1개의 데이터를 제거한 후,
// 삭제한 index 위치에 현재 드래그하고 있는 item 데이터를 넣는다.
// -> 삭제된 요소들의 배열은 prevItem 변수에 저장된다.
const prevItem = coppiedStateArray.splice(hoverIndex, 1, dragItem);
// 4. 3번과 마찬가지의 과정을 거친 후
coppiedStateArray.splice(dragIndex, 1, prevItem[0]);
// 5. coppiedStateArray 배열을 return
return coppiedStateArray;
});
}
};
const returnItemsForColumn = (columnName: string) => {
return items
.filter((item: any) => item.column === columnName)
.map((item, index) => (
<MovableItem
key={item.id}
name={item.name}
setItems={setItems}
index={index}
moveCardHandler={moveCardHandler}
/>
));
};
const { DO_IT, IN_PROGRESS, AWAITING_REVIEW, DONE } = COLUMN_NAMES;
return (
<div className="dnd-container">
<DndProvider backend={HTML5Backend}>
<Column title={DO_IT} className="dnd-column dnd-first-column">
{returnItemsForColumn(DO_IT)}
</Column>
<Column title={IN_PROGRESS} className="dnd-column dnd-second-column">
{returnItemsForColumn(IN_PROGRESS)}
</Column>
<Column title={AWAITING_REVIEW} className="dnd-column dnd-first-column">
{returnItemsForColumn(AWAITING_REVIEW)}
</Column>
<Column title={DONE} className="dnd-column dnd-second-column">
{returnItemsForColumn(DONE)}
</Column>
</DndProvider>
</div>
);
};
export default DraggableComponent;
Column.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { FC } from "react";
import { useDrop } from "react-dnd";
import { COLUMN_NAMES, ITEM_TYPE } from "constants/dnd";
import { ColumnProps } from "types/DragAndDrop";
const Column: FC<ColumnProps> = ({ children, className, title }: ColumnProps) => {
const [, drop] = useDrop({
accept: ITEM_TYPE,
drop: () => ( { name: title } ),
});
return (
<div ref={drop} className={className}>
{title}
{children}
</div>
);
};
export default Column;
MovableItem.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
import { ItemState } from "types/DragAndDrop";
import { COLUMN_NAMES, ITEM_TYPE } from "constants/dnd";
const MovableItem = ({ name, index, moveCardHandler, setItems }: any) => {
const changeItemColumn = (currentItem: any, columnName: string) => {
setItems((prevState: ItemState[]) =>
prevState.map((e: ItemState) => {
return {
...e,
column: e.name === currentItem.name ? columnName : e.column,
};
})
);
};
const ref = useRef<HTMLDivElement>(null);
// 마우스가 hover item 높이의 절반을 넘을 경우에만 이동을 수행하도록 설계
// - 아래로 드래그할 때 커서가 50% 이하일 때만 이동
// - 위로 드래그할 때 커서가 50% 이상일 때만 이동
const [, drop] = useDrop({
accept: ITEM_TYPE,
hover(item: { index: number; name: string }, monitor: any) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// 아래로 드래깅
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// 위로 드래깅
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
moveCardHandler(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: ITEM_TYPE,
item: { index, name },
end: (item, monitor) => {
const dropResult: any = monitor.getDropResult();
if (dropResult) {
const { name } = dropResult;
const { DO_IT, IN_PROGRESS, AWAITING_REVIEW, DONE } = COLUMN_NAMES;
switch (name) {
case DO_IT:
changeItemColumn(item, DO_IT);
break;
case IN_PROGRESS:
changeItemColumn(item, IN_PROGRESS);
break;
case AWAITING_REVIEW:
changeItemColumn(item, AWAITING_REVIEW);
break;
case DONE:
changeItemColumn(item, DONE);
break;
}
}
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0.4 : 1;
drag(drop(ref));
return (
<>
<div ref={ref} className="dnd-movable-item" style=>
{name}
</div>
</>
);
};
export default MovableItem;
tasks.ts
1
2
3
4
5
6
7
8
9
10
import { COLUMN_NAMES } from "constants/dnd";
const { DO_IT } = COLUMN_NAMES;
export const tasks = [
{ id: 1, name: "Item 1", column: DO_IT },
{ id: 2, name: "Item 2", column: DO_IT },
{ id: 3, name: "Item 3", column: DO_IT },
{ id: 4, name: "Item 4", column: DO_IT },
];
dnd.ts
1
2
3
4
5
6
7
8
export const COLUMN_NAMES = {
DO_IT: "Do it",
IN_PROGRESS: "In Progress",
AWAITING_REVIEW: "Awaiting review",
DONE: "Done",
};
export const ITEM_TYPE = "BOARD_VIEW";
DragAndDrop.ts
1
2
3
4
5
6
7
8
9
10
11
export interface ItemState {
id: number;
name: string;
column: string;
}
export interface ColumnProps {
children: React.ReactNode;
className: string;
title: string;
}
Leave a comment