[react-beautiful-dnd] drag and drop 구현
1. DragDropContext 태그
: 드래그 앤 드롭을 가능하게 허용할 부분을 감싸주는 태그
: props 속성에는 onDragStart 와 onDragEnd(필수) 가 있다.
: onDragEnd 에는 요소를 드래그 한 후 드롭했을 때 실행할 함수를 지정한다.
function App() {
return (
<div className="App">
<header className="App-header">
<h1> Final Space Characters</h1>
<DragDropContext onDragEnd={handleEnd}>
...
</DragDropContext>
</header>
</div>
);
2. onDragEnd 에 지정할 함수 작성
const handleEnd = (result) => {
//result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함.
console.log(result);
// 목적지가 없으면 이 함수를 종료합니다.
if(!result.destination) return;
// 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
const items = Array.from(characters);
// 1. 변경시키는 아이템을 배열에서 지워주기
// 2. return 값으로 지워진 아이템을 잡아주기
const [reorderedItem] = items.splice(result.source.index,1);
// 원하는 자리에 reorderedItem 을 insert 해준다.
items.splice(result.destination.index, 0, reorderedItem);
setCharacters(items);
}
1. Array.from() 을 사용하여 원본 배열을 복사하고 items 라는 변수에 저장한다.
2. splice 메소드를 이용하여 변경시키는 아이템의 위치에 해당하는 값을 배열(item)에서 삭제하고, 해당 값을 reorderedItem 에 저장한다.
3. 변경시키고자 하는 위치에 reorderedItem 의 값을 삽입한다. splice() 메소드에서 두 번째 매개변수가 0이면 어떤 요소도 삭제하지 않고 새로운 요소를 삽입만 한다.
4. 위와 같은 과정을 수행하면 드래그 앤 드랍을 통해 아이템의 위치를 변경할 수 있다.
splice 메소드
array.splice(startIndex, deleteCount, item1, item2, ...)
- startIndex: 수정을 시작할 인덱스 위치 (필수)
- deleteCount: 제거할 요소의 개수 (선택)
- item1, item2, ...: 추가할 새로운 요소들 (선택)
- splice() 는 원본 배열을 직접 수정하며 제거된 요소들을 새로운 배열로 반환한다.
➡️ 궁금했던 점
Q. 어차피 받고 싶은 값은 하나인데 왜 굳이 구조분해할당을 이용해서 값을 저장할까?
const reorderedItem = items.splice(result.source.index,1); 이렇게 해도 되는거 아닌가?
A. 구조 분해 할당을 사용하는 이유는 splice () 메서드가 항상 배열을 반환하기 때문이다.
< 예시>
const fruits = ['apple', 'banana', 'orange'];
// 일반적인 할당
const removed = fruits.splice(1, 1);
console.log(removed); // ['banana'] (배열)
console.log(typeof removed); // object (배열)
// 구조분해 할당
const [removedItem] = fruits.splice(1, 1);
console.log(removedItem); // 'banana' (문자열)
console.log(typeof removedItem); // string
<차이점 설명>
1. 일반 할당
const reorderedItem = items.splice(result.source.index, 1);
// reorderedItem은 ['item'] 형태의 배열
2. 구조분해 할당
const [reorderedItem] = items.splice(result.source.index, 1);
// reorderedItem은 'item' 형태의 단일 값
3. Droppable 태그
: 드롭이 가능한 부분을 감싸주는 태그
: droppableId => 구분자(필수 태그)
<Droppable droppableId="characters">
{(provided) => (/* ... */)}
</Droppable>
드롭 가능한 영역을 정의한다. 고유 id 를 characters 로 설정한다.
Q. provided 는 무엇인가?
A. provided는 Droppable 컴포넌트의 children으로 전달되는 함수의 매개변수이다. react-beautiful-dnd 라이브러리가 내부적으로 이 provided 객체를 생성하여 전달한다.
<ul
className="characters"
{...provided.droppableProps}
ref={provided.innerRef}
>
- provided.droppableProps: 드래그 앤 드롭에 필요한 속성들
- ref={provided.innerRef}: DOM 요소 참조를 위한 ref
{characters.map(({ id, name }, index) => {
return (/* Draggable 컴포넌트 */);
})}
characters 배열 매핑 : 각 캐릭터를 드래그 가능한 요소로 변환한다.
4. Draggable 태그
: draggableId => 구분자 (필수 속성)
: index => 정렬을 위한 데이터
: Draggable 이 내부 함수에 전달해주는 인자 => provided
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (/* ... */)}
</Draggable>
- key: React의 리스트 렌더링을 위한 고유 키
- draggableId: 드래그 요소의 고유 ID
- index: 드래그 요소의 순서
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<p>{name}</p>
</li>
- provided.draggableProps: 드래그 기능을 위한 기본적인 속성들을 포함
// 실제로는 이런 속성들이 자동으로 추가됨
<li
data-rbd-draggable-context-id="0"
data-rbd-draggable-id="task-1"
style={{
position: 'relative',
transform: 'translate(0px, 0px)'
}}
onTransitionEnd={...}
>
- provided.dragHandleProps: 실제 드래그 동작을 처리하는 이벤트 핸들러들을 포함
// 실제로는 이런 이벤트 핸들러들이 자동으로 추가됨
<li
onMouseDown={...}
onKeyDown={...}
onTouchStart={...}
tabIndex={0}
aria-grabbed="false"
>
5. provided.placeholder
{provided.placeholder}
- 드래그 중인 아이템의 공간을 유지하기 위한 요소
- 드래그 중인 아이템이 position: fixed로 설정되어 원래 위치에서 벗어나면서 생기는 공간을 채워준다.
- 반드시 드롭 영역의 마지막에 위치해야 한다.
전체 코드
import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
const finalSpaceCharacters = [
{
id: "gary",
name: "Gary Goodspeed",
},
{
id: "cato",
name: "Little Cato",
},
{
id: "kvn",
name: "KVN",
},
];
function App() {
const [characters, setCharacters] = useState(finalSpaceCharacters);
const handleEnd = (result) => {
//result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함.
console.log(result);
// 목적지가 없으면 이 함수를 종료합니다.
if(!result.destination) return;
// 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
const items = Array.from(characters);
// 1. 변경시키는 아이템을 배열에서 지워주기
// 2. return 값으로 지워진 아이템을 잡아주기
const [reorderedItem] = items.splice(result.source.index,1);
// 원하는 자리에 reorderedItem 을 insert 해준다.
items.splice(result.destination.index, 0, reorderedItem);
setCharacters(items);
}
return (
<div className="App">
<header className="App-header">
<h1> Final Space Characters</h1>
<DragDropContext onDragEnd={handleEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul
className="characters"
{...provided.droppableProps}
ref={provided.innerRef}
>
{characters.map(({ id, name }, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<p>{name}</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</header>
</div>
);
}
export default App;
🌟 배운 점
오늘은 react-beautiful-dnd 라는 라이브러리를 사용하여 드래그 앤 드랍 기능을 구현해봤다. 라이브러리를 사용하니 훨씬 간단하게 사용할 수 있었던 반면에, 이해가 안되는 부분들이 많아서 많이 찾아봤다. 검색을 해보면서 코드 한 줄 한 줄 이해하려고 노력했다. 드래그 앤 드랍 기능은 모든 웹사이트에서 사용하는 것을 쉽게 볼 수 있으니 구현하는 방법을 잘 기억해두고 써먹어야겠다. 그리고 드래그 앤 드랍을 구현할 수 있는 다른 방법들도 찾아봐야겠다.