react-dnd → Drag & DropFrontend/UI&UX2022. 6. 30. 10:56
Table of Contents
react-dnd
- react-dnd는 react에서 쉽게 Drag & Drop을 사용할 수 있게 해주는 라이브러리이다.
- 모바일에서 작동하지 않는 drag 이벤트를 touch 이벤트로 바꿔주기 때문에 모바일 환경에서도 Drag & Drop 기능을 사용할 수 있게 해준다.
- 또한 Drag중인 요소를 추적해서 css 속성을 바꿔주거나 Drag가 끝난 상황도 커스터마이징 해줄 수 있는 유용한 라이브러리이다!
- 예를 들어 아래 그림처럼 드래그중일 때 opacitiy와 같은 css 속성을 넣고 state를 수정해서 요소의 순서를 바뀌게 할 수 있다!
- 아래 공식 문서와 여러가지 example code를 보고 참조하자! 내가 간단히 구현한 것보다 훨씬 많은 기능을 추가할 수 있다!
https://react-dnd.github.io/react-dnd/docs/overview
https://codesandbox.io/examples/package/react-dnd
사용해보기
라이브러리 추가하기
- 우선 react-dnd와 drag 이벤트를 touch 이벤트로 바꿔서 모바일에서 사용하게 해주는 react-dnd-multi-backend 라이브러리를 설치하자.
- react-dnd-html5-backend는 drag 이벤트를 처리하고 react-dnd-touch-backend는 touch 이벤트를 처리한다.
yarn add react-dnd react-dnd-multi-backend react-dnd-html5-backend react-dnd-touch-backend
DndProvider 설정하기
- react-dnd를 사용할 container(drag and drop 요소들을 포함하는 컨테이너)를 DndProvider로 감싸고 options에 HTML5toTouch를 넣어서 웹과 모바일 환경에서 모두 drag and drop을 사용하게 설정하자.
import { DndProvider } from 'react-dnd-multi-backend';
// for mobile
import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch';
import {Container} from "./Container";
const App = () => {
return (
<div>
{/*
multi-backend의 DndProvider를 사용하고 options에 HTML5toTouch를 넣으면
모바일에서 처리할 수 없는 drag 이벤트를 touch 이벤트로 바꿔줌!
react-dnd를 사용할 컨테이너를 DndProvider로 감싸줌
*/}
<DndProvider options={HTML5toTouch}>
<Container/>
</DndProvider>
</div>
);
}
export default App;
Container 구현하기
- dnd를 편하게 쓰기 위해 state의 list에서 각 요소들을 식별할 수 있는 id값을 넣자.
- state의 list에서 각 요소들의 id로 해당 요소와 index를 리턴하는 함수(findCard)를 작성하고 이 정보를 가지고 state를 변경해서 drag중인 요소가 어떤 한 요소 위를 hover할 때 서로의 위치를 바꾸는 함수(changeCard)도 작성하자.
- state에서 요소들 사이의 index가 변경되면 map을 사용해서 렌더링할 때 순서가 바뀌겟지요!?
- state에서 {id: 1,text: 'duckgugong'}가 0번째 인덱스고 {id: 2,text: 'hungry'}가 1번째 인덱스면
{id:1, text: 'duckgugong'}인 Card를 drag해서 {id:2, text: 'hungry'}에 hover하면 {id:1, text: 'duckgugong'}가 1번째 인덱스가 되고 {id:2, text: 'hungry'}가 0번째 인덱스가 된다!
- 그리고 실질적으로 각 요소가 drag될 때 findCard 함수와 changeCard를 이용해서 state의 list 내부에서 요소들간의 index를 변경시키게 하기 위해 props로 두 함수를 넘겨주자
import update from 'immutability-helper'
import { memo, useCallback, useState } from 'react'
import { Card } from './Card.js'
const style = {
display: "flex",
flexWrap: "wrap"
}
const ITEMS = [
{
id: 1,
text: 'duckgugong',
},
{
id: 2,
text: 'hungry',
},
{
id: 3,
text: 'sleepy',
},
{
id: 4,
text: 'chicken',
},
{
id: 5,
text: 'hamburger',
},
{
id: 6,
text: '???',
}
]
// DND를 담는 컨테이너
export const Container = memo(function Container() {
const [cards, setCards] = useState(ITEMS);
// Card의 id에 해당하는 Card와 인덱스 리턴
// {id:1, text:"duckgugong"}이 0번 인덱스면 {id: 1, text:"duckgugong"}, 0 리턴
const findCard = useCallback(
(id) => {
const card = cards.filter((item) => `${item.id}` === id)[0]
return {
card,
index: cards.indexOf(card),
}
},
[cards],
)
/*
Card의 위치 교환.
state에서 {id: 1,text: 'duckgugong'}가 0번째 인덱스고 {id: 2,text: 'hungry'}가 1번째 인덱스면
{id:1, text: 'duckgugong'}인 Card를 drag해서 {id:2, text: 'hungry'}에 hover하면
{id:1, text: 'duckgugong'}가 1번째 인덱스가 되고 {id:2, text: 'hungry'}가 0번째 인덱스가 된다!
*/
const moveCard = useCallback(
(id, atIndex) => {
const { card, index } = findCard(id)
setCards(
update(cards, {
$splice: [
[index, 1],
[atIndex, 0, card],
],
}),
)
},
[findCard, cards, setCards],
)
return (
<div style={style}>
{cards.map((card) => (
<Card
key={card.id}
id={`${card.id}`}
text={card.text}
moveCard={moveCard}
findCard={findCard}
/>
))}
</div>
)
})
Card 구현하기
- 문맥상 drag할 수 있는 각 요소들을 Card라고 부를 것이다!
- 아래에 주석을 친절히? 달아 놨으니 꼭 읽어보길 바란다!
import {memo} from 'react'
import {useDrag, useDrop} from 'react-dnd'
const style = {
border: '1px solid blue',
margin: '0.5rem',
width: '100px',
padding: '0.5rem 1rem',
backgroundColor: 'white',
cursor: 'pointer',
flexShrink:"0"
}
export const Card = memo(function Card({id, text, moveCard, findCard}) {
// Card의 id로 원래 인덱스를 찾기
const originalIndex = findCard(id).index;
// drag 여부를 판별하는 isDragging과 drag할 요소에 부착할 ref를 받음
const [{isDragging}, dragRef] = useDrag(
() => ({
// drag할 요소의 type을 지정
type: "CARD",
// Container에서 props로 넘겨준 요소의 id와 id를 가지고 state 내의 실제 index를
// Card가 사용할 수 있도록 넘겨준다.
item: {id, originalIndex},
// collect 옵션을 넣지 않으면 dragging 중일 때 opacity가 적용되지 않는다!
collect: (monitor) => ({
// isDragging 변수가 현재 드래깅중인지 아닌지를 true/false로 리턴한다
isDragging: monitor.isDragging(),
}),
}),
[originalIndex],
)
const [, dropRef] = useDrop(
() => ({
// CARD 타입만 허용. 즉 useDrag와 타입이 다르면 아무 일도 일어나지 않음!
accept: "CARD",
// 요소를 드래그해서 다른 요소 위에서 hover할 때 자신이 아니면 위치를 바꿈!
// useDrag에서 item으로 지정한 id와 index를 가지고 위치를 교환!
hover({id: draggedId}) {
if (draggedId !== id) {
// hover된 요소와 index 교환! -> 위치 교환
const {index: overIndex} = findCard(id);
moveCard(draggedId, overIndex);
}
},
}),
[findCard, moveCard],
)
return (
// dragRef와 dropRef 장착
<div ref={(node) => dragRef(dropRef(node))} style={{...style, opacity: isDragging ? 0.4 : 1}}>
{text}
</div>
)
})
실행 결과
'Frontend > UI&UX' 카테고리의 다른 글
이미지&비디오 여러개 업로드 - 미리보기/시간제한/삭제 (0) | 2022.07.05 |
---|---|
비디오&이미지 업로더 - 미리보기/시간 제한 (0) | 2022.07.01 |
tailwind-css (0) | 2022.06.11 |
무한 스크롤 - infinite scroll (0) | 2022.06.11 |
이미지 업로드, 미리보기 (0) | 2021.11.19 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!