HTML 드래그 앤 드롭 API + React

HTML 드래그 앤 드롭 API를 이용하여 사용자는 draggable요소를 마우스로 선택해 droppable 요소로 드래그 하고 마우스 버튼에서 손을 뗌으로써 요소를 드롭할 수 있다. 드래그 하는 동안 draggable 요소는 반투명한 채로 마우스 포인터를 따라다닌다.

드래그 이벤트

드래그 앤 드롭은 DOM event model과 drag events를 mouse events로부터 상속받는다.

draggable 요소를 마우스로 선택하고, 마우스 포인터를 droppable요소로 가져가 마우스 버튼을 떼는 것으로 이루어진다.

다음과 같은 드래그와 관련된 속성이 있다.

이벤트이벤트 핸들러설명
dragondrag요소나 텍스트 블록을 드래그 할 때 발생한다.
dragstartondragstart사용자가 요소나 텍스트 블록을 드래그하기 시작했을 때 발생한다.
dragendondragend드래그를 끝냈을 때 발생한다. (마우스 버튼을 떼거나 ESC 키를 누를 때
dragenterondragenter드래그한 요소나 텍스트 블록을 적합한 드롭 대상위에 올라갔을 때 발생한다.
dragexitondragexit요소가 더 이상 드래그의 직접적인 대상이 아닐 때 발생한다.
dragleaveondragleave드래그하는 요소나 텍스트 블록이 적합한 드롭 대상에서 벗어났을 때 발생한다.
dragoverondragover요소나 텍스트 블록을 적합한 드롭 대상 위로 지나갈 때 발생한다. (짧은 간격의 ms마다 한 번씩 발생한다)
dropondrop요소나 텍스트 블록을 적합한 드롭 대상에 드롭했을 때 발생한다.

드래그 앤 드롭 적용하기

드래그 앤 드롭을 리액트에 적용해보았다.

1 요소를 draggable로 설정하기

요소를 만들고 요소를 draggable로 설정해주어야 한다.

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
import React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

2 드래그 시작 이벤트 추가하기

드래그가 시작될 때 어떤 요소로부터 시작되었는지 추적해야한다.

즉 dragStart이벤트의 target을 추적해야한다.

useRef를 사용하여 드래그가 시작된 아이템을 저장하였다.

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
import React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const dragItem = useRef();
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

3 드래그 하는 동안 건너뛰는 다른 요소 추적하기

드래그 하는 동안 다른 드래그 가능한 요소를 지날 경우 이 요소를 저장해야 한다.

즉 dragenter가 제일 최근에 일어난 요소를 저장해야한다.

useRef를 사용하여 저장하였다.

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
import React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const dragItem = useRef();
const dragOverItem = useRef();
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

const dragEnter = (e, position) => {
dragOverItem.current = position;
};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

4 드래그가 끝났을 경우의 이벤트 추가하기

앞서 useRef에 저장해둔 두 요소를 바꾸는 이벤트를 추가한다.

즉 dragEnd가 되었을 때의 이벤트를 추가한다.

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
import React, { useState, useRef } from "react";
import "./App.css";

const App = () => {
const dragItem = useRef();
const dragOverItem = useRef();
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

const dragEnter = (e, position) => {
dragOverItem.current = position;
};

const drop = (e) => {
const copyListItems = [...list];
const dragItemContent = copyListItems[dragItem.current];
copyListItems.splice(dragItem.current, 1);
copyListItems.splice(dragOverItem.current, 0, dragItemContent);
dragItem.current = null;
dragOverItem.current = null;
setList(copyListItems);
};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
onDragEnd={drop}
key={index}
draggable
>
{item}
</div>
))}
</>
);
};
export default App;

5 dragover시 기본 애니메이션 효과를 제거한다.

dragover에 preventDefault를 호출해야 드롭을 허용할 수 있다.

정확히는 드롭이 허용된 애니메이션을 줄 수 있다.

1
onDragOver={(e) => e.preventDefault()}

++ dragEnter 될 떄마다 바뀌도록 하기

dragEnter 핸들러와 dragEnd 핸들러를 합치고 요소가 바뀔 때마다 dragItem이 dragOverItem을 참조하도록 하여 dragEnter 이벤트에 배열이 바뀌도록 설정할 수도 있다.

요소가 다른 요소를 통과할 때마다 배열이 바뀐다.

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
function App() {
const dragItem = useRef(null);
const dragOverItem = useRef(null);
const [list, setList] = useState([
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
]);

const dragStart = (e, position) => {
dragItem.current = position;
};

const dragEnter = (e, position) => {
dragOverItem.current = position;
const copyListItems = [...list];
const dragItemContent = copyListItems[dragItem.current];
copyListItems.splice(dragItem.current, 1);
copyListItems.splice(dragOverItem.current, 0, dragItemContent);
dragItem.current = dragOverItem.current;
dragOverItem.current = null;
setList(copyListItems);
};

// const drop = (e: React.DragEvent<HTMLDivElement>, position: number) => {};

return (
<>
{list &&
list.map((item, index) => (
<div
style={{
backgroundColor: "lightblue",
margin: "20px 25%",
textAlign: "center",
fontSize: "40px",
}}
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
// onDragEnd={drop}
onDragOver={(e) => e.preventDefault()}
key={index}
draggable
>
{item}
</div>
))}
</>
);
}

export default App;