본문 바로가기

클라이언트/React
[리액트(React)] 드래그 앤 드롭(drag & drop)

// 드래그 앤 드롭, drag & drop

- 마우스나 터치스크린을 이용해서 객체를 이동시킬 수 있게 하는 기술


// react-beautiful-dnd 

- drag & drop 을 구현하기 위한 라이브러리

- DragDropContext, Droppable, Draggable의 세 가지 주요 컴포넌트로 구성되어 있다.

  • DragDropContext : 전반적인 프레임워크
  • Droppable : 드롭 가능한 영역
  • Draggable : 드래그 가능한 각 요소

1. 설치

npm install react-beautiful-dnd


타입스크립트를 사용한다면 @types도 추가로 설치한다.

npm install --save-dev @types/react-beautiful-dnd

2. DragDropContext 사용

<DragDropContext onDragEnd={handleDragEnd}>
  <!-- children 영역 -->
</DragDropContext>
  • onDragEnd : 드래그가 끝난 후 호출되는 함수
    - 매개변수로 드래그된 객체에 대한 정보를 받을 수 있다.
  • children : jsx 코드

3. Droppable 사용

<Droppable droppableId="">{(provided) => jsx코드}</Droppable>
  • children : jsx 코드를 바로 사용할 수 없고, 함수를 호출하여 jsx 코드를 return 한다.
  • provided : 자체적으로 제공되는 DroppableProvided. 
    - provided.placeholder를 사용하여 리스트 사이즈를 유지할 수 있다.

4. Draggable 사용

<Draggable key={} draggableId={} index={}>
  {(provided) => jsx코드}
</Draggable>
  • key와 draggableId의 값은 같아야 한다.
  • children : droppable의 children과 마찬가지로 jsx 코드를 바로 사용할 수 없고, 함수를 호출하여 jsx 코드를 return 한다.
    - jsx 코드에 props로
       ref={provided.innerRef}
       {...provided.dragHandleProps}
       {...provided.draggableProps}
      를 사용한다.

 

import DraggableCard from './DraggableCard';
import { Droppable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { useForm } from 'react-hook-form';
import { IToDo, toDoState } from '../store/atoms';
import { RecoilLoadable, useSetRecoilState } from 'recoil';

const Wrapper = styled.div`
  width: 300px;
  padding-top: 10px;
  background-color: ${(props) => props.theme.boardColor};
  border-radius: 5px;
  min-height: 300px;
  display: flex;
  flex-direction: column;
`;

const Title = styled.h2`
  text-align: center;
  font-weight: 600;
  margin-bottom: 10px;
  margin-top: 10px;
  font-size: 18px;
  color: #a90404;
`;

interface IAreaProps {
  isDraggingOver: boolean;
  isDraggingFromThisWith: boolean;
}

const Area = styled.div<IAreaProps>`
  background-color: ${(props) =>
    props.isDraggingOver
      ? '#ffb8b1'
      : props.isDraggingFromThisWith
        ? '#ffe0e0'
        : 'transparent'};
  flex-grow: 1;
  transition: background-color 0.3s ease-in-out;
  padding: 20px;
`;

const Form = styled.form`
  width: 100%;
  input {
    width: 100%;
  }
`;

interface IBoardProps {
  toDos: IToDo[];
  boardId: string;
}

interface IForm {
  toDo: string;
}

function Board({ toDos, boardId }: IBoardProps) {
  const setToDos = useSetRecoilState(toDoState);
  const { register, setValue, handleSubmit } = useForm<IForm>();
  const onValid = ({ toDo }: IForm) => {
    const newToDo = {
      id: Date.now(),
      text: toDo,
    };
    setToDos((allBoards) => {
      return { ...allBoards, [boardId]: [...allBoards[boardId], newToDo] };
    });
    setValue('toDo', '');
  };
  return (
    <Wrapper>
      <Title>{boardId}</Title>
      <Form onSubmit={handleSubmit(onValid)}>
        <input
          {...register('toDo', { required: true })}
          type="text"
          placeholder={`Add task on ${boardId}`}
        />
      </Form>
      <Droppable droppableId={boardId}>
        {(provided, info) => (
          <Area
            isDraggingOver={info.isDraggingOver}
            isDraggingFromThisWith={Boolean(info.draggingFromThisWith)}
            ref={provided.innerRef}
            {...provided.droppableProps}
          >
            {toDos.map((toDo, index) => (
              <DraggableCard
                key={toDo.id}
                toDoId={toDo.id}
                toDoText={toDo.text}
                index={index}
              />
            ))}
            {provided.placeholder}
          </Area>
        )}
      </Droppable>
    </Wrapper>
  );
}

export default Board;

 

import { Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import React from 'react';

const Card = styled.div<{ isDragging: boolean }>`
  border-radius: 5px;
  margin-bottom: 5px;
  padding: 10px 10px;
  background-color: ${(props) => props.theme.cardColor};
  box-shadow: ${(props) =>
    props.isDragging ? '0px 2px 5px rgb(116 71 71 / 50%)' : 'none'};
`;

interface IDraggableCardProps {
  toDoId: number;
  toDoText: string;
  index: number;
}

function DraggableCard({ toDoId, toDoText, index }: IDraggableCardProps) {
  return (
    <Draggable draggableId={`${toDoId}`} index={index}>
      {(provided, snapshot) => (
        <Card
          isDragging={snapshot.isDragging}
          ref={provided.innerRef}
          {...provided.dragHandleProps}
          {...provided.draggableProps}
        >
          {toDoText}
        </Card>
      )}
    </Draggable>
  );
}

export default React.memo(DraggableCard);

// React.memo

드래그를 시작하면 위치가 변하지 않은 컴포넌트들까지 모두 재렌더링이 된다.

이를 막기 위해서 React.memo로 Draggable 컴포넌트를 감싸준다.

React.memo(Draggable 컴포넌트명)

import { Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import React from 'react';

const Card = styled.div`
  border-radius: 5px;
  margin-bottom: 5px;
  padding: 10px 10px;
  background-color: ${(props) => props.theme.cardColor};
`;

interface IDraggableCardProps {
  toDo: string;
  index: number;
}

function DraggableCard({ toDo, index }: IDraggableCardProps) {
  return (
    <Draggable key={toDo} draggableId={toDo} index={index}>
      {(provided) => (
        <Card
          ref={provided.innerRef}
          {...provided.dragHandleProps}
          {...provided.draggableProps}
        >
          {toDo}
        </Card>
      )}
    </Draggable>
  );
}

export default React.memo(DraggableCard);

※ React 18 에서는 <React.StrictMode>를 제거해야 된다.

※ 현재 react-beautiful-dnd는 업데이트가 중단되었다.

차차 다른 라이브러리를 찾아서 글을 수정할 예정..