개발

Reactjs - 드래그 앤 드롭 리스트 구현하기

syaku 2024. 11. 6. 14:55
반응형

React에서 드래그 앤 드롭 리스트 구현하기 (MUI + React-DnD)

레이어 목록을 드래그하여 순서를 변경할 수 있는 컴포넌트를 구현해보겠습니다. Material-UI와 React-DnD를 활용하여 직관적인 UI와 부드러운 드래그 앤 드롭 기능을 구현할 수 있습니다.

필요한 패키지 설치

# Material-UI 관련 패키지
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled

# React-DnD 관련 패키지
npm install react-dnd react-dnd-html5-backend

# 불변성 관리를 위한 패키지
npm install immutability-helper

주요 기능

  • 드래그 앤 드롭으로 항목 순서 변경
  • 최상단/최하단 이동 버튼
  • 한 칸 위/아래 이동 버튼
  • 드래그 중인 항목의 시각적 피드백

컴포넌트 구조

  1. LayerList: 메인 컴포넌트
  2. Layer: 개별 드래그 가능한 항목 컴포넌트

전체 코드 설명

import React, { useState, useCallback } from 'react';
import {
  List,
  ListItem,
  ListItemIcon,
  IconButton,
  Box
} from '@mui/material';
import {
  DragIndicator,
  KeyboardDoubleArrowUp,
  KeyboardDoubleArrowDown,
  KeyboardArrowUp,
  KeyboardArrowDown
} from '@mui/icons-material';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import update from 'immutability-helper';

// 드래그 가능한 아이템 컴포넌트
const Layer = ({ id, text, index, moveItem, moveToTop, moveToBottom, moveUp, moveDown }) => {
  const ref = React.useRef(null);

  // 드래그 로직 설정
  const [{ isDragging }, drag] = useDrag({
    type: 'item',
    item: { id, index },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  // 드롭 로직 설정
  const [, drop] = useDrop({
    accept: 'item',
    hover: (item, monitor) => {
      if (!ref.current) return;

      const dragIndex = item.index;
      const hoverIndex = index;
      if (dragIndex === hoverIndex) return;

      moveItem(dragIndex, hoverIndex);
      item.index = hoverIndex;
    },
  });

  // ref 설정으로 드래그&드롭 기능 활성화
  drag(drop(ref));

  return (
    <ListItem
      ref={ref}
      style={{
        opacity: isDragging ? 0.5 : 1,
        cursor: 'move',
        backgroundColor: '#fff',
        marginBottom: '4px',
        border: '1px solid #eee',
        borderRadius: '4px'
      }}
    >
      {/* 왼쪽 드래그 핸들 */}
      <ListItemIcon>
        <DragIndicator />
      </ListItemIcon>

      {/* 아이템 내용 */}
      <Box sx={{ flex: 1 }}>{text}</Box>

      {/* 오른쪽 이동 버튼들 */}
      <Box>
        <IconButton size="small" onClick={() => moveToTop(index)}>
          <KeyboardDoubleArrowUp />
        </IconButton>
        <IconButton size="small" onClick={() => moveUp(index)}>
          <KeyboardArrowUp />
        </IconButton>
        <IconButton size="small" onClick={() => moveDown(index)}>
          <KeyboardArrowDown />
        </IconButton>
        <IconButton size="small" onClick={() => moveToBottom(index)}>
          <KeyboardDoubleArrowDown />
        </IconButton>
      </Box>
    </ListItem>
  );
};

// 메인 LayerList 컴포넌트
const LayerList = () => {
  // 초기 아이템 데이터
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' },
    { id: 4, text: 'Item 4' },
  ]);

  // 드래그로 아이템 이동
  const moveItem = useCallback((dragIndex, hoverIndex) => {
    setItems((prevItems) =>
      update(prevItems, {
        $splice: [
          [dragIndex, 1],
          [hoverIndex, 0, prevItems[dragIndex]],
        ],
      }),
    );
  }, []);

  // 최상단으로 이동
  const moveToTop = useCallback((index) => {
    setItems((prevItems) => {
      const newItems = [...prevItems];
      const [removed] = newItems.splice(index, 1);
      newItems.unshift(removed);
      return newItems;
    });
  }, []);

  // 최하단으로 이동
  const moveToBottom = useCallback((index) => {
    setItems((prevItems) => {
      const newItems = [...prevItems];
      const [removed] = newItems.splice(index, 1);
      newItems.push(removed);
      return newItems;
    });
  }, []);

  // 한칸 위로 이동
  const moveUp = useCallback((index) => {
    if (index === 0) return;
    setItems((prevItems) => {
      const newItems = [...prevItems];
      [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];
      return newItems;
    });
  }, []);

  // 한칸 아래로 이동
  const moveDown = useCallback((index) => {
    setItems((prevItems) => {
      if (index === prevItems.length - 1) return prevItems;
      const newItems = [...prevItems];
      [newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]];
      return newItems;
    });
  }, []);

  return (
    <DndProvider backend={HTML5Backend}>
      <List sx={{ width: '100%', maxWidth: 600, margin: '0 auto' }}>
        {items.map((item, index) => (
          <Layer
            key={item.id}
            id={item.id}
            text={item.text}
            index={index}
            moveItem={moveItem}
            moveToTop={moveToTop}
            moveToBottom={moveToBottom}
            moveUp={moveUp}
            moveDown={moveDown}
          />
        ))}
      </List>
    </DndProvider>
  );
};

export default LayerList;

주요 코드 설명

1. Layer 컴포넌트

드래그 가능한 개별 항목을 표현하는 컴포넌트입니다.

const Layer = ({ id, text, index, moveItem, moveToTop, moveToBottom, moveUp, moveDown }) => {
  const ref = React.useRef(null);

  // useDrag: 드래그 기능 설정
  const [{ isDragging }, drag] = useDrag({
    type: 'item',
    item: { id, index },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  // useDrop: 드롭 기능 설정
  const [, drop] = useDrop({
    accept: 'item',
    hover: (item, monitor) => {
      if (!ref.current) return;
      const dragIndex = item.index;
      const hoverIndex = index;
      if (dragIndex === hoverIndex) return;
      moveItem(dragIndex, hoverIndex);
      item.index = hoverIndex;
    },
  });

  // drag와 drop 기능을 ref에 연결
  drag(drop(ref));

  // ... JSX 반환
};

2. 이동 관련 함수들

// 드래그로 아이템 이동
const moveItem = useCallback((dragIndex, hoverIndex) => {
  setItems((prevItems) =>
    update(prevItems, {
      $splice: [
        [dragIndex, 1],
        [hoverIndex, 0, prevItems[dragIndex]],
      ],
    }),
  );
}, []);

// 최상단으로 이동
const moveToTop = useCallback((index) => {
  setItems((prevItems) => {
    const newItems = [...prevItems];
    const [removed] = newItems.splice(index, 1);
    newItems.unshift(removed);
    return newItems;
  });
}, []);

// 한칸 위로 이동
const moveUp = useCallback((index) => {
  if (index === 0) return;
  setItems((prevItems) => {
    const newItems = [...prevItems];
    [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];
    return newItems;
  });
}, []);

주요 라이브러리 설명

React-DnD

  • useDrag: 드래그 가능한 요소를 정의
  • useDrop: 드롭 가능한 영역을 정의
  • DndProvider: 드래그 앤 드롭 컨텍스트 제공

Material-UI (MUI)

  • List/ListItem: 리스트 구조 제공
  • IconButton: 이동 버튼 구현
  • Box: 레이아웃 컨테이너

immutability-helper

  • 불변성을 유지하면서 배열/객체 업데이트
  • update() 함수로 효율적인 상태 업데이트

스타일링

<ListItem
  style={{
    opacity: isDragging ? 0.5 : 1,
    cursor: 'move',
    backgroundColor: '#fff',
    marginBottom: '4px',
    border: '1px solid #eee',
    borderRadius: '4px'
  }}
>

사용 방법

import LayerList from './components/LayerList';

function App() {
  return (
    <div>
      <LayerList />
    </div>
  );
}

이 컴포넌트는 드래그 앤 드롭으로 항목을 재정렬할 수 있으며, 버튼을 통해 항목의 위치를 직접 조정할 수 있습니다. Material-UI의 디자인 시스템을 활용하여 깔끔하고 일관된 UI를 제공합니다.

반응형