React Dnd kit 정리 - 드래그 앤 드롭으로 위치 바꾸기
1. React Dnd kit란?
React에서 드래그 앤 드롭 인터페이스를 구축하기 위한 패키지 세트.
끌어서 놓을 수 있는 요소를 만들고 이벤트를 관리하고 상호 작용을 구현하도록 도움을 준다.
2. 설치
npm install @dnd-kit/core
npm install react react-dom
Modifiers
이동 좌표를 동적으로 수정할 수 있는 기능을 제공한다.
- 단일 축 동작 제한
- 드래그 가능한 노드 컨테이너, 스크롤 컨테이너의 사각형 경계로 이동 제한
- 모션 고정 적용 등..
이와 같은 기능들을 구현하기 위해서는 modifiers의 DndContext, DraggableClone을 활용해야 한다.
npm install @dnd-kit/modifiers
드래그 앤 드롭 기능을 사용하기 위해서는 <DndContext> 내부에 기능을 구현해야 한다.
구체적으로 컨테이너가 작동하는 방식을 정의할 수 있다.
Sortable
정렬가능한 인터페이스를 구현하려고 하는 경우 권장되는 패키지.
npm install @dnd-kit/sortable
드래그 앤 드롭 기능을 구현하기 위해서는 컴포넌트들이 반드시 <DndContext> 내부에 위치할 수 있도록 해야한다.
import React from 'react';
import {DndContext} from '@dnd-kit/core';
import {Draggable} from './Draggable';
import {Droppable} from './Droppable';
function App() {
return (
<DndContext>
<Draggable/>
<Droppable/>
</DndContext>
)
}
3. 요소끼리 위치 바꾸도록 구현하기(세로)
드래그 요소를 리스트 형태로 렌더링하여 드래그를 통해 서로의 위치를 바꿀 수 있도록 구현해보자.
<App.js>
import React, { useState } from 'react';
import UserComponent from './UserComponent';
import {
closestCenter,
DndContext,
PointerSensor,
useSensor,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
function App() {
const [items, setItems] = useState([ // 요소들의 정보와 위치 변화 관리용 State
{
id: '1',
name: 'Banana',
},
{
id: '2',
name: 'Kimchi',
},
{
id: '3',
name: 'Potato',
},
{
id: '4',
name: 'New Jeans',
},
{
id: '5',
name: 'Apple',
},
]);
const sensors = [useSensor(PointerSensor)]; // 마우스 포인터로 동작 지정
const handleDragEnd = ({ active, over }) => { // 드래그가 끝날 때(드롭할 때) 이벤트 리스너
if (active.id !== over.id) { // active는 움직일 때, over는 끝날 때(드롭할 때)
// 요소가 움직일 때
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id); // 현재 움직이는 요소의 index
const newIndex = items.findIndex((item) => item.id === over.id); // 드롭한 위치에 있는 요소의 index
return arrayMove(items, oldIndex, newIndex); // (배열, 이전 index, 드롭 위치 index를 받아서 요소들 간의 위치를 swap하는 메서드)
});
}
};
return (
<div
style={{
margin: 'auto',
width: 200,
textAlign: 'center',
}}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter} // 드래그 충돌 감지 영역 지정
onDragEnd={handleDragEnd} // 드래그 끝날 때(드롭할 때) 이벤트 리스너 호출
>
<SortableContext
items={items.map((item) => item.id)} // 요소들의 id값 할당
strategy={verticalListSortingStrategy} // 요소들의 정렬 방식 설정
>
{items.map((item) => (
<UserComponent {...item} key={item.id} /> // 요소들을 배치함
))}
</SortableContext>
</DndContext>
</div>
);
}
export default App;
3.1 DndContext
DndContext에 전달해주어야 할 값은 다음과 같다.
- seonser
- collisionDetection
- onDragEnd
sensor는 키보드, 마우스 등과 같이 특정 요소를 어떻게 드래그 앤 드롭을 할 것인지 정의를 해주는 부분을 의미한다.
useSensor hook을 사용하여 단일 센서를 포함한 배열로 정의되어야 한다.
마우스 포인터를 이용할 경우 [useSensor(PointerSensor)]로 정의한다.
이렇게 하면 드래그 앤 드롭을 통한 상호작용과 상태 업데이트를 구현할 수 있다.
collisionDectection은 그냥 컨테이너 간의 충돌 처리를 어떻게 할 것인지에 대한 전략을 설정하는 부분이다.
예를들어 closestCenter라고 설정하면, 해당 컨테이너의 중간 부분에서 충돌 처리가 나도록 하는 것이다.
onDragEnd는 드래그가 끝나면 호출되는 함수를 명시하는 부분이다.
3.2 SortableContext
SortableContext는 항목들을 정렬하거나 위치를 바꾸는 등의 기능을 구현하기 위해서 사용된다.
SortableContext에 전달할 값들은 다음과 같다.
- items
- strategy
items에는 Array의 각 요소에 대한 id 값을 전달한다. Map을 활용하면 된다.
strategy는 item들의 배열 방식을 전달한다.
수직 방향으로 할 경우 verticalListSortingStrategy를 활용,
수평 방향의 경우 horizontalListSortingStrategy를 활용한다. -> 이 방법이 잘 되지 않는 경우, 4번 항목 참조.
{items.map((item) => (
<UserComponent {...item} key={item.id} /> // 요소들을 배치함
))}
여기서 {item}이 아니라 {...item}을 사용한 이유는 스프레드 확장자를 사용해야 개체의 각 속성을 컴포넌트로 전달할 수 있기 때문이다.
items에는 name과 id값 두 가지 속성이 있기 때문에 UserComponent.js에서 이 값들에 접근하도록 하기 위해서 스프레드 확장자를 사용했다고 이해하면 된다.
return arrayMove(items, oldIndex, newIndex); // (배열, 이전 index, 드롭 위치 index를 받아서 요소들 간의 위치를 swap하는 메서드)
arrayMove는 @dnd-kit/sortable 패키지에서 제공되는 메서드이다.
배열의 항목을 한 인덱스에서 다른 인덱스로 이동하는 데 사용할 수 있는 메서드로서 다음 세 가지 인수를 취한다.
- 이동할 항목의 배열
- 이동할 항목의 현재 인덱스
- 항목을 이동시킬 인덱스
이 메서드가 호출되면 항목간의 인덱스가 변경된 새 배열이 반환되는 것이다.
<UserComponent.js>
npm install @dnd-kit/utilities
import { useSortable } from '@dnd-kit/sortable';
import React from 'react';
import { CSS } from '@dnd-kit/utilities';
const UserComponent = ({ id, name }) => {
const {
setNodeRef,
attributes,
listeners,
transition,
transform,
isDragging,
} = useSortable({ id: id });
const style = {
transition,
transform: CSS.Transform.toString(transform),
border: '2px solid black',
marginBottom: 5,
marginTop: 5,
opacity: isDragging ? 0.5 : 1,
};
return (
<div ref={setNodeRef} {...attributes} {...listeners} style={style}>
{name}
</div>
);
};
export default UserComponent;
3.3 useSortable
useSortable에는 id 속성에 외부에서 전달받은 id값을 넣어준다.
useSortable로부터 추출할 값들은 다음과 같다.
1. setNodeRef : DndContext에서 제공하는 드래그 앤 드롭 기능을 활성화 하는 데 필요한 컴포넌트의 DOM 노드에 대한 참조를 만든다.
2. attributes
default값으로 다음과 같은 속성들이 포함되어 있다.
role : 어떤 형태를 띄는지 쉽게 이해할 수 있도록 지정한다. ex) list item
tableIndex: 0으로 설정되면 키보드에 의해 요소들의 초점을 맞출 수 있다.
aria-roledescription : sortable item으로 설정하면 요소의 역할에 대한 더 자세한 설명을 제공한다.
위의 세가지 속성은 html의 기본 속성이므로 자세한 것은 생략하고, 명시하지 않아도 잘 작동한다.
role 명시의 예
return (
<div
ref={setNodeRef}
{...attributes}
role="listitem"
{...listeners}
style={style}
>
{name}
</div>
);
attributes는 반드시 스프레드 확장자로 제공되어야 한다. 그래야 드래그 가능한 요소를 찾을 수 있고 필요한 기능을 할 수 있기 때문이다.
3. listeners
리스너도 마찬가지로 스프레드 확장자로 제공하도록 하자.
4. transition : 드래그 앤 드롭 상호작용 중에 적용되는 CSS 전환. default는 null로, 전환이 적용되지 않는다.
사용하려면 transition: "transform 200ms ease-in-out"과 같은 방식으로 활용하면 되겠다. 위의 코드에서는 느려터진 효과가 나올테니 그냥 null로 두는 것을 추천함.
5. transform : 드래그 할 수 있는 요소의 현재 변환 상태를 x, y와 같은 값으로 나타낸다.
CSS.Transform.toString() 메서드는 transform 개체를 CSS trnasform 속성 값으로 사용할 수 있는 문자열로 변환하기 위해 사용된다. transform 속성 자체가 문자열 값을 필요로 하기 때문이다.
이를 적용하면 해당 요소를 드래그 할 때 마우스 포인터의 위치로 해당 요소가 따라오는 효과를 낼 수 있다.
6. isDragging : 현재 요소가 드래그 중일 때 true를 반환한다.
4. 요소끼리 위치 바꾸도록 구현하기(가로)
App.js의 스타일을 다음과 같이 변경했다.
<div
style={{
margin: 'auto',
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
}}
>
그냥 flex로 두고 가운데 정렬만 했다.
UserComponents의 스타일을 다음과 같이 변경했다.
const style = {
transition,
transform: CSS.Transform.toString(transform),
border: '2px solid black',
marginLeft: 10,
marginTop: 10,
opacity: isDragging ? 0.5 : 1,
width: 100,
};
각 요소의 크기를 적당히 100으로 하고 margin을 윗방향과 왼쪽 방향으로 넣었다.
5. 가로, 세로 그리드 형태에서 적용하기
마찬가지로 스타일만 수정하면 쉽게 적용할 수 있다.
<App.js>
import React, { useState } from 'react';
import UserComponent from './UserComponent';
import {
closestCenter,
DndContext,
PointerSensor,
useSensor,
} from '@dnd-kit/core';
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
function App() {
const [items, setItems] = useState([
{
id: '1',
name: 'Banana',
},
{
id: '2',
name: 'Kimchi',
},
{
id: '3',
name: 'Potato',
},
{
id: '4',
name: 'New Jeans',
},
{
id: '5',
name: 'Apple',
},
{
id: '6',
name: 'Orange',
},
{
id: '7',
name: 'Pineapple',
},
{
id: '8',
name: 'Watermelon',
},
{
id: '9',
name: 'Grapes',
},
{
id: '10',
name: 'Pear',
},
{
id: '11',
name: 'Peach',
},
{
id: '12',
name: 'Cherry',
},
{
id: '13',
name: 'Blueberry',
},
{
id: '14',
name: 'Mango',
},
{
id: '15',
name: 'Lemon',
},
{
id: '16',
name: 'Lime',
},
]);
const sensors = [useSensor(PointerSensor)];
const handleDragEnd = ({ active, over }) => {
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
return (
<div
style={{
marginTop: '20px',
margin: 'auto',
width: '300px',
height: '300px',
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gridGap: '10px',
}}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items.map((item) => item.id)}>
{items.map((item) => (
<UserComponent {...item} key={item.id} />
))}
</SortableContext>
</DndContext>
</div>
);
}
export default App;
<UserComponent.js>
import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
const UserComponent = ({ id, name }) => {
const {
setNodeRef,
attributes,
listeners,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
border: '2px solid black',
padding: '5px',
textAlign: 'center',
opacity: isDragging ? 0.5 : 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
};
return (
<div ref={setNodeRef} {...attributes} {...listeners} style={style}>
{name}
</div>
);
};
export default UserComponent;