Javascript/React

React DnD 라이브러리

kyoulho 2024. 7. 31. 18:34

React에서는 react-dnd 라이브러리를 사용하여 드래그 앤 드롭 기능을 간편하게 구현할 수 있다.

설치

먼저, react-dnd와 react-dnd-html5-backend를 설치한다. react-dnd-html5-backend는 HTML5의 드래그 앤 드롭 API를 사용하는 기본 백엔드이다.

npm i react-dnd react-dnd-html5-backend
npm i -D @types/react-dnd

React DnD에서 백엔드는 드래그 앤 드롭 이벤트를 처리하고, 드래그된 아이템의 상태를 관리하는 시스템이다. "백엔드"라는 용어는 여기서 드래그 앤 드롭 기능을 지원하는 '기술적 메커니즘' 또는 '엔진'을 의미한다. 실제 서버 백엔드와 혼동할 수 있지만, 문맥상 이 백엔드는 클라이언트 측에서 동작하는 드래그 앤 드롭의 내부 동작 방식을 의미한다.
다양한 백엔드를 사용할 수 있지만, 가장 흔히 사용되는 백엔드는 HTML5의 드래그 앤 드롭 API를 사용하는 HTML5 Backend이다. 이 백엔드는 마우스와 터치 이벤트를 처리하고, 브라우저의 기본 드래그 앤 드롭 기능을 활용한다. 다른 백엔드를 사용할 수도 있는데, 예를 들어 터치 장치나 커스텀 렌더링 환경을 지원하는 백엔드가 있다.

기본 설정

React DnD를 사용하려면 애플리케이션의 루트 컴포넌트를 DndProvider로 감싸야한다. DndProvider는 드래그 앤 드롭 콘텍스트를 제공한다.

import React from 'react';
import ReactDOM from 'react-dom';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import App from './App';

ReactDOM.render(
  <DndProvider backend={HTML5Backend}>
    <App />
  </DndProvider>,
  document.getElementById('root')
);

useDrag

useDrag 훅은 요소를 드래그 가능하게 만든다. useDrag 훅을 사용하여 드래그 소스의 설정을 정의할 수 있다.

import React from 'react';
import { useDrag } from 'react-dnd';

// 드래그 가능한 아이템 타입 정의
const ItemTypes = {
  CARD: 'card',
};

// Card 컴포넌트는 드래그 가능한 카드 아이템을 렌더링한다
const Card = ({ id, text }) => {
  // useDrag 훅을 사용하여 드래그 상태와 드래그 참조 설정
  const [{ isDragging }, drag] = useDrag({
    type: ItemTypes.CARD, // 드래그 아이템의 타입 설정
    item: { id }, // 드래그 시 전송될 아이템 데이터 설정
    collect: (monitor) => ({
      isDragging: monitor.isDragging(), // 드래그 중인지 여부를 수집
    }),
  });

  return (
    // ref={drag}는 이 div 요소를 드래그 소스로 설정한다
    <div
      ref={drag}
      style={{
        opacity: isDragging ? 0.5 : 1, // 드래그 중이면 반투명하게 표시
        cursor: 'move', // 커서를 이동 아이콘으로 변경
      }}
    >
      {text} {/* 카드 텍스트를 표시 */}
    </div>
  );
};

export default Card;

useDrop

useDrop 훅은 요소가 드래그된 아이템을 받을 수 있게 만든다. useDrop훅을 사용하여 드롭 대상의 설정을 정의할 수 있다.

import React from 'react';
import { useDrop } from 'react-dnd';

// 드롭 가능한 아이템 타입 정의
const ItemTypes = {
  CARD: 'card',
};

// DropArea 컴포넌트는 드래그 가능한 카드 아이템을 드롭할 수 있는 영역을 렌더링한다
const DropArea = ({ onDrop }) => {
  // useDrop 훅을 사용하여 드롭 상태와 드롭 참조를 설정한다
  const [{ isOver }, drop] = useDrop({
    accept: ItemTypes.CARD, // 이 드롭 영역에서 받아들일 아이템 타입을 설정
    drop: (item) => onDrop(item.id), // 아이템이 드롭되었을 때 호출될 함수, 아이템의 id를 인자로 전달
    collect: (monitor) => ({
      isOver: monitor.isOver(), // 드롭 영역 위에 드래그 중인지 여부를 수집
    }),
  });

  return (
    // ref={drop}는 이 div 요소를 드롭 영역으로 설정한다
    <div
      ref={drop}
      style={{
        height: '100px', // 드롭 영역의 높이 설정
        width: '100px', // 드롭 영역의 너비 설정
        backgroundColor: isOver ? 'lightgreen' : 'lightgray', // 드래그 중일 때와 아닐 때의 배경색 설정
      }}
    >
      Drop here {/* 드롭 영역의 텍스트 표시 */}
    </div>
  );
};

export default DropArea;

드래그 앤 드롭 기능 통합

위의 두 컴포넌트를 통합하여 드래그 앤 드롭 기능을 완성한다.

import React, { useState } from 'react';
import Card from './Card';
import DropArea from './DropArea';

const App = () => {
  const [droppedItem, setDroppedItem] = useState(null);

  const handleDrop = (id) => {
    setDroppedItem(id);
  };

  return (
    <div>
      <Card id="1" text="Draggable Card" />
      <DropArea onDrop={handleDrop} />
      {droppedItem && <p>Dropped item: {droppedItem}</p>}
    </div>
  );
};

export default App;

useDragLayer

useDragLayer 훅은 커스텀 드래그 레이어를 만들 때 사용된다. 드래그 중인 아이템의 커스텀 미리 보기를 렌더링 하는 데 사용된다.

import React from 'react';
import { useDragLayer } from 'react-dnd';

// CustomDragLayer 컴포넌트는 커스텀 드래그 미리보기를 렌더링한다
const CustomDragLayer = () => {
  // useDragLayer 훅을 사용하여 드래그 상태와 아이템 위치를 설정한다
  const { item, isDragging, currentOffset } = useDragLayer((monitor) => ({
    item: monitor.getItem(), // 드래그 중인 아이템을 가져온다
    isDragging: monitor.isDragging(), // 드래그 중인지 여부를 가져온다
    currentOffset: monitor.getClientOffset(), // 드래그 중인 아이템의 현재 좌표를 가져온다
  }));

  // 드래그 중이 아니면 아무것도 렌더링하지 않는다
  if (!isDragging) {
    return null;
  }

  // 커스텀 드래그 레이어의 스타일을 설정한다
  const layerStyles = {
    position: 'fixed', // 고정 위치
    pointerEvents: 'none', // 포인터 이벤트 비활성화
    zIndex: 100, // z-index 설정
    left: currentOffset.x, // 현재 좌표의 x값 설정
    top: currentOffset.y, // 현재 좌표의 y값 설정
  };

  return (
    // 커스텀 드래그 레이어를 렌더링한다
    <div style={layerStyles}>
      <div style={{ transform: 'rotate(5deg)' }}>
        {item.text} {/* 드래그 중인 아이템의 텍스트 */}
      </div>
    </div>
  );
};

export default CustomDragLayer;

useDragDropManager

useDragDropManager 훅은 드래그 앤 드롭 매니저에 접근할 수 있게 한다. 매니저를 통해 드래그 앤 드롭 시스템의 상태와 설정을 제어할 수 있다.

import React from 'react';
import { useDragDropManager } from 'react-dnd';

// DragDropStatus 컴포넌트는 현재 드래그 상태를 표시한다
const DragDropStatus = () => {
  // useDragDropManager 훅을 사용하여 DragDropManager 인스턴스를 가져온다
  const dragDropManager = useDragDropManager();
  // DragDropManager의 모니터를 가져온다
  const monitor = dragDropManager.getMonitor();
  // 현재 드래그 중인지 여부를 가져온다
  const isDragging = monitor.isDragging();

  return (
    <div>
      {/* 현재 드래그 상태에 따라 메시지를 표시한다 */}
      {isDragging ? 'An item is being dragged' : 'No items are being dragged'}
    </div>
  );
};

export default DragDropStatus;

 

DraggableList.tsx

import type {DivProps} from "./Div";
import type {FC} from "react";
import {useRef} from "react";
import {useDrag, useDrop} from "react-dnd";
import type {Identifier} from 'dnd-core';

export type MoveFunc = (dragIndex: number, hoverIndex: number) => void

export type ListDraggableProps = DivProps & {
    id: any
    index: number
    onMove: MoveFunc
}

interface DragItem {
    index: number
    id: string
    type: string
}

// ListDraggable 컴포넌트는 드래그 앤 드롭 기능을 제공하는 리스트 아이템을 렌더링한다
export const ListDraggable: FC<ListDraggableProps> = ({ id, index, onMove, style, className, ...props }) => {
    // ref는 드래그 및 드롭을 위해 div 요소를 참조한다
    const ref = useRef<HTMLDivElement>(null);

    // useDrop 훅을 사용하여 드롭 대상 설정
    const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
        accept: 'list', // 'list' 타입의 아이템을 받아들임
        collect(monitor) {
            return {
                handlerId: monitor.getHandlerId() // 드롭 대상의 핸들러 ID 수집
            }
        },
        hover(item: DragItem) {
            if (!ref.current) {
                return;
            }
            const dragIndex = item.index;
            const hoverIndex = index;

            // 드래그 중인 아이템의 인덱스와 호버 중인 아이템의 인덱스가 같으면 아무 작업도 하지 않음
            if (dragIndex === hoverIndex) {
                return;
            }

            // 아이템 이동 함수 호출
            onMove(dragIndex, hoverIndex);
            // 드래그 중인 아이템의 인덱스를 호버 중인 인덱스로 업데이트
            item.index = hoverIndex;
        }
    });

    // useDrag 훅을 사용하여 드래그 소스 설정
    const [{ isDragging }, drag] = useDrag({
        type: 'list', // 'list' 타입의 아이템
        item: () => {
            return { id, index };
        },
        collect: monitor => ({
            isDragging: monitor.isDragging() // 드래그 중인지 여부 수집
        })
    });

    // 드래그 중일 때 투명도 설정
    const opacity = isDragging ? 0 : 1;

    // ref를 드래그 및 드롭 훅에 연결
    drag(drop(ref));

    return (
        <div
            ref={ref}
            {...props}
            className={[className, 'cursor-move'].join(' ')} // 커서 모양을 이동 커서로 설정
            style={{ ...style, opacity }} // 스타일과 투명도 설정
            data-handler-id={handlerId} // 데이터 핸들러 ID 설정
        />
    );
};

'Javascript > React' 카테고리의 다른 글

리액트 라우터  (0) 2024.08.01
React Beautiful DnD 라이브러리  (0) 2024.07.31
리덕스 미들웨어  (0) 2024.07.29
리듀서 활용하기  (0) 2024.07.22
리덕스 기본 개념 이해하기  (2) 2024.07.21