Drag and Drop을 만들어보자!
React로 list를 만들기 위한 아름답고 접근 가능한 드래그 앤 드롭
설치방법
: 터미널에npm i react-beautiful-dnd
입력
우리는 typescript에서 사용할 것이므로 추가로 터미널에npm i --save-dev @types/react-beautiful-dnd
해줘야함.
react beautiful dnd는 DragDropContext와 Droppable, Dragggable이 있다.
먼저 DragDropContext는 드래그앤드롭을 가능하게 하고 싶은 앱의 한 부분임~!
DragDropContext
는 onDragEnd
를 필요로 한다.
onDragEnd
함수는 유저가 드래그를 끝낸 시점에 불려지는 함수임.
우리가 어떤 것을 드롭할 수 있는 영역을 말함.
Droppable은 droppableId를 필요로 한다.
유저가 드롭할 수 있는 영역이 두가지 이상일 수 있기 때문!
추가로 droppable의 children은 함수 형태여야 함,,!
우리가 어떤 것을 드래그할 수 있는 영역을 말한다.
Droppable과 마찬가지로 draggableId를 필요로 하고 children이 함수형태여야한다~! 추가로 index도 필요로 한다.
각각의 리스트는 무언가를 드롭할 수 있는 영역으로 droppable한다. 리스트에 있는 item들은 draggable하겠지?!
(아, 이 세가지는 모두 import해줘야 사용가능)
droppable과 draggable의 첫번째 argument로 이름은 그냥 마음대로 지어줘도 된다.
강의에서는 magic으로 지어주었으므로 걍 나는 magic을 사용했다.
draggable의 경우 만약 요소가 기본적으로 드래그되길 원한다면 draggableProps
를 하면 되는데, dragHandleProps
를 쓰면 원하는 곳 어디서든지 카드를 집어 드래그할 수 있다.
return (
<DragDropContext onDragEnd={onDragEnd}>
<div>
<Droppable droppableId='one'>
{(magic) => (
<ul ref={magic.innerRef} {...magic.droppableProps}>
<Draggable draggableId='first' index={0}>
{(magic) =>
<li
ref={magic.innerRef}
{...magic.draggableProps}
{...magic.dragHandleProps}
>
One
</li>
}
</Draggable>
<Draggable draggableId='second' index={1}>
{(magic) =>
<li
ref={magic.innerRef}
{...magic.draggableProps}
{...magic.dragHandleProps}
>
Two
</li>
}
</Draggable>
</ul>
)}
</Droppable>
</div>
</DragDropContext>
);
만약 유저로 하여금 li를 어떤 위치에서든지 드래그해서 옮기게 하고 싶다면 li에 draggableProps와 dragHandleProps를 넣어주면 된다고 함.
dragHandleProps를 이용하면 드래그 적용되게 하는 범위를 설정할 수 있음~,~
(ref 빼먹지말자!)
이렇게 해주고 검사창에서 elements를 확인해보면
위와 같이 prop들이 매우 많이 생긴 것을 알 수 있다.
이것들은 모두 우리가 어떤 것을 드래그하기 위해 필요한 prop들이라고 함!
이제 우리 App 실행 화면을 보면!
화면에 뭔가 마우스가 손바닥 모양이 되었을 것이다.
One과 Two가 이제 드래그가 될 것이다.. 움직인다.
근데 이대로만 해주면 드롭해줬을 때 바로 원상복귀된다.
이건 우리가 드롭이 끝났을 때 할 일을 정해주지 않았기 때문이다.
const toDos = ["a", "b", "c", "d", "e", "f"];
...
{toDos.map((toDo, index) => <Draggable draggableId={toDo} index={index}>
{(magic) =>
<Card
ref={magic.innerRef}
{...magic.draggableProps}
{...magic.dragHandleProps}
>
{toDo}
</Card>
}
</Draggable>
)}
...
이런식으로 써주면 draggable을 array로 만들 수 있다.
map 이용하기 ㅋ.ㅋ
+) 추가로 코드에 {magic.placeholder} 추가하면 draggable이 빠져나가도 리스트의 사이즈는 그대로 유지된다! 유용한 도우미.
위에서는 그냥 toDos로 선언해주었던 리스트를 toDoState라는 atom으로 선언해주자.
export const toDoState = atom({
key: "toDo",
default: ["a", "b", "c", "d", "e", "f"],
});
그리고 useRecoilState를 사용하자!
내가 하고싶은 것은 드래그가 끝났을 때 리스트의 인덱스를 변화시키는 것이다. (리스트 재정렬)
이를 위해서는 예전의 index값과 드래그 직후 인덱스 값을 알아야한다.
onDragEnd에 주목하자..
const [toDos, setToDos] = useRecoilState(toDoState);
const onDragEnd = ({ destination, source }: DropResult) => {};
onDragEnd에서 source(최근 드래그된 항목의 원래 index)와 destination(바꾸고싶은 인덱스)을 받아온다.
알고리즘은 이렇게된다.
먼저 source를 그 위치에서 삭제한다. 그 후 destination 위치의 index에 우리가 방금 삭제한 걸 넣는다!
const x = [ "a","b","c","d","e","f"];
의 코드가 있고 우리가 예를 들어 a, 즉 index 0을 지우고 싶다고 해보자.
이 경우, x.splice(0,1)
하면!
splice는 index 0으로 가서 1개의 아이템을 삭제한다.
splice는 이런식으로 작동한다.👍
이게 첫번째 단계이다.
그럼 이제 우리가 a를 index2에 넣고 싶다고 해보자.
그럼 다시 x.splice를 해서 a를 2번 자리에 넣고 싶다고 말해야한다.
이 경우 x.splice(2,0,"a")
를 해주면 된다.
이 코드는 index 2에서 시작해서 어떤 것도 지우지 말고 a를 집어넣으라는 말과 같당
그럼 우리는 splice를 이용해서 우리가 원하는 결과를 만들어낼 수 있겠다!!!!!!
한 번 해보자.
onDragEnd 함수를 수정한다.
먼저 현재의 값을 가져와야겠지? 이름은 oldToDos
로 해주자.
그리고 우리는 새롭게 수정된 array를 return해주어야한다.
모든 toDos를 변형시킬 수 없기 때문에
const toDosCopy = [...oldToDos];
<< 이렇게 복사본을 만들어주자.
이제 source.index에서 item을 삭제하자.
toDosCopy.splice(source.index, 1)
해주면 우리의 item은 삭제된다.
그럼 이제 이 item을 다시 destination.index에 넣자.
toDosCopy.splice(destination?.index, 0, draggableId);
추가로 destination이 아니면 걍 return하라는 조건도 달아준다.
이제 재정렬이 잘 될 것이다~!
그러나 지금은 다시 렌더링하는 과정에서 약간의 간극이 생긴다. 뒤틀려보임,,,
다음엔 이걸 해결해보자.