Frontend/UI&UX

react-dnd → Drag & Drop

덕구공 2022. 6. 30. 10:56

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

 

React DnD

 

react-dnd.github.io

https://codesandbox.io/examples/package/react-dnd

 

react-dnd examples - CodeSandbox

 

codesandbox.io

 

사용해보기

라이브러리 추가하기

  • 우선 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>
  )
})

실행 결과