만들면서 이해하는 DnD (feat.React-ts)

SOPLAY·2023년 6월 26일
4

react를 다루면서 react-dnd와 같은 다양한 드래그-앤-드랍 라이브러리들이 존재하지만 아주 간단하게 핵심 로직만 직접 React + typesciprt로 구현해 보겠습니다!

1. 데이터 타입 정하기

DnD만들 구현하더라도 이를 구현하기 위한 데이터가 필요하기 때문에 데이터의 타입을 지정하겠습니다

export type IDragData = {
  id: number;
  title: string;
  type: 'doing' | 'done';
};

이를 기반으로 IDragData[]의 형태를 가지는 데이터를 만들어서 조작하면 될 거 같습니다.!


2. useDragData() hooks 구현

이름을 어떻게 정할까 고민히다가 useDragData()라고 정했지만 다른 이름으로 사용하셔도 문제가 없습니다..!

구현을 하면서 필요한 {filteredData,changeType, addItem}를 구현하겠습니다.

import { useState } from 'react';
//초기 데이터로 사용 할 값 입니다.
const initialState: IDragData[] = [
  { id: 1, title: '진행중 테스트1', type: 'doing' },
  { id: 2, title: '진행중 테스트2', type: 'doing' },
  { id: 3, title: '진행중 테스트3', type: 'doing' },
  { id: 4, title: '진행중 테스트4', type: 'doing' },
  { id: 5, title: '진행중 테스트5', type: 'doing' },
  { id: 6, title: '완료 테스트1', type: 'done' },
  { id: 7, title: '완료 테스트2', type: 'done' },
];


const useDragData = () => {
  const [dragData, setDragData] = useState<IDragData[]>(initialState);

  const filteredData = () =>
    dragData.reduce(
      (acc, cur) => {
        acc[cur.type].push(cur);
        return acc;
      },
      { doing: [] as IDragData[], done: [] as IDragData[] }
    );

  const changeType = ({
    id,
    type,
  }: {
    id: IDragData['id'];
    type: IDragData['type'];
  }) => setDragData(dragData.map(v => (v.id === id ? { ...v, type } : v)));

  const addItem = ({
    title,
    type,
  }: {
    title: IDragData['title'];
    type: IDragData['type'];
  }) =>
    setDragData([
      ...dragData,
      { id: dragData[dragData.length - 1].id + 1, title, type },
    ]);

  return { filteredData, changeType, addItem };
};

export default useDragData;

3. 화면에 보여주기 위한 ItemList, ItemCard 컴포넌트 구현

ItemList.tsx

type IItemList = {
  title: string;
  data: IDragData[];
  onDragEnter: (event: React.DragEvent<HTMLDivElement>) => void;
};

const ItemList = ({ title, data }: IItemList) => (
  <div
    className={'border-2 p-3 w-64 border-black category'}
    data-category={title}
	onDragEnter={onDragEnter}
  >
    <h2 className={'border-b-2 text-xl text-center font-bold'}>
      {title.toUpperCase()}
    </h2>
    <ul>
      {data.map(v => (
        <ItemCard {...v} key={`item-card-${v.id}`} />
      ))}
    </ul>
  </div>
);

export default ItemList;

ItemCard.tsx

해당 요소는 드래그가 가능해야 하기 때문에 draggable을 주었습니다!

import { IDragData } from '../hooks/useDragData.ts';

const ItemCard = ({ title, id }: IDragData) => (
  <li
    className={`px-2 py-1 mt-2 border rounded bg-white`}
    data-id={id}
    draggable
  >
    <p className={'flex justify-between items-center'}>
      <span>{title}</span>
      <span className={'text-xs text-gray-400'}>Id:{id}</span>
    </p>
  </li>
);

export default ItemCard;

4. DnD 컴포넌트 구현

이번의 목표는 DnD를 간단하게 확인하려하기 때문에 DragTestComponent.tsx에 모든 로직을 다 집어넣었지만 실제로 적용을 하려면 ref를 통한 useDrag() hooks를 통한 구현을 추천드립니다...!

DragTestComponents.tsx

import { useCallback, useState } from 'react';
import useDragData from '../hooks/useDragData.ts';
import ItemList from './ItemList.tsx';

const DragTestComponent = () => {
  const { filteredData, changeType } = useDragData();
  const data = filteredData();

  const [currentDragItem, setCurrentDragItem] = useState(-1);

  const onDragStart = useCallback(
    (event: React.DragEvent<HTMLDivElement>) => {
      const { id } = (event.target as HTMLElement).dataset;
      currentDragItem !== +(id || -1) && setCurrentDragItem(+(id || -1));
    },
    [currentDragItem]
  );
  const onDragEnd = useCallback(() => {
    setCurrentDragItem(-1);
  }, []);

  const onDragEnter = useCallback(
    (event: React.DragEvent<HTMLDivElement>) => {
      const target = (
        event.target === event.currentTarget
          ? event.target
          : (event.target as HTMLElement).closest('div.category')
      ) as HTMLElement;

      const { category } = target.dataset;
      changeType({ id: currentDragItem, type: category as 'doing' | 'done' });
    },
    [currentDragItem]
  );

  const onDragOver = useCallback(
    (event: React.DragEvent<HTMLDivElement>) => event.preventDefault(),
    []
  );

  return (
    <div
      className={'flex gap-20'}
      onDragOver={onDragOver}
      onDragStart={onDragStart}
      onDragEnd={onDragEnd}
    >
      {Object.keys(data).map(type => (
        <ItemList
          title={type}
          data={data[type as keyof typeof data]}
          key={`dnd-category-${type}`}
          onDragEnter={onDragEnter}
        />
      ))}
    </div>
  );
};
export default DragTestComponent;

완성!

최종적으로 만들어진 결과물은 다음과 같습니다!!
구현을 진행하며 인덱스의 위치도 변경시킬 수 있게 할까 고민했지만 이는 제외하고 칸반을 이동하는 형태로 만들었습니다.!

프로젝트 소스코드


profile
꾸준히 발전하는 개발자

2개의 댓글

comment-user-thumbnail
2023년 7월 2일

https://melonplaymods.com/2023/06/11/bowl-with-fruits-and-vegetables-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/f4-phantom-attack-jet-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/panzerkampfwagen-viii-maus-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/pak-on-the-police-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/police-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/railway-car-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/two-storey-residential-building-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/opila-bird-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/naruto-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/inside-a-titan-shifters-nape-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/half-life-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/flamethrower-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/brawl-stars-pack-characters-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/super-heroes-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/swords-from-terraria-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/grave-stone-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/pirate-cannon-pirate-cannon-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/motorcycle-mod-for-melon-playground-3/
https://melonplaymods.com/2023/06/10/gta-protagonists-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/project-playtime-mod-for-melon-playground-2/

답글 달기
comment-user-thumbnail
2023년 7월 2일

https://melonplaymods.com/2023/06/10/ppg-miscellaneous-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/tank-m4-sherman-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/laboratory-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/star-wars-pack-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/flowers-in-pots-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/ghost-face-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/cudly-and-mutant-cudly-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/bunker-2-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/anti-personnel-gear-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/e-100-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/god-of-war-35-items-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/doors-mod-for-melon-playground-2/
https://melonplaymods.com/2023/06/10/m134-minigun-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/serro-ultraman-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/dark-background-with-stars-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/jotaro-star-platinum-v3-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/characters-in-metal-gear-rising-revengeance-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/3selfpropelled-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/maxim-machine-gun-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/yyds-mod-for-melon-playground/

답글 달기