[React] Using dnd library in React

Beanxx·2024년 8월 17일
0

React

목록 보기
6/6
post-thumbnail

회사에서 dnd 기능을 사용해야 하는 업무가 있어서 적용해보고 다른 라이브러리로 변경하여 동작 개선까지 해본 사용기 !

‣ 구현 기능 : 어드민 페이지 내 자사 앱 내 메뉴 표시 순서를 dnd 동작으로 자유롭게 변경이 가능해야 함
‣ 구현 기간:
1. 2024.04 1차 기능 구현
2. 2024.08 라이브러리 변경하여 dnd 동작 개선 작업 진행

1. dnd 라이브러리 선정

처음 dnd 동작을 적용하려고 react 호환 라이브러리르 찾아봤을 때 가장 대중적으로 사용하는 3개의 라이브러리 중에서 선택하려고 했다. 각 라이브러리 특징에 대해 간단히 비교해보고 정했다.

1️⃣ react-draggable

  • 드래그 가능한 구성 요소를 만드는데 매우 단순화된 라이브러리
  • 다운로드 수는 가장 많지만, 드래그 가능한 구성 요소의 위치가 변경될 때마다 DOM을 변경하며, 테이블 구현에 적합하지 않다

⇒ 즉, 드래그로 아이템 간의 순서 변경보다는 윈도우즈에서 윈도우를 드래그해서 위치를 바꾸는 부분에서 강점이 있는 라이브러리로 Drop 가능한 영역에 대해서는 직접 구현해줘야 하므로 패쓰..

2️⃣ react-dnd

  • DOM을 조작하지 않으며, 드래그 가능한 구성 요소를 위해 만들어진 상태 관리 라이브러리라고 보면 된다
  • 내부적으론 Redux를 사용하고 있으며, 아이템 간의 순서 변경 등의 드래그 액션이 편리한 UX를 만들 수 있는 환경에서 주로 사용된다.

3️⃣ react-beautiful-dnd

  • react-dnd 기반으로 만들어진 라이브러리로, UI/UX나 퍼포먼스가 좋은 동작이 미리 정의되어 있다.
  • react-dnd보다 용량이 약 2배 많으며, 커스터마이징할 수 있는 폭이 적다.

🙌 위의 각 라이브러리만의 특징들을 비교하여 비교적 용량이 작고, 구현하고 하는 기능을 커스텀마이징하여 구현 가능한 두번째의 react-dnd 라이브러리를 선정하게 되었다.



2. react-dnd 라이브러리 적용

1) 우선 react-dnd 라이브러리를 사용하기 위해서는 라이브러리 설치부터!

npm install react-dnd react-dnd-html5-backend

2) 이후 dnd를 사용할 페이지 상위에 `DndProvider` 적용해주기

‣ 나는 App.tsx Route 선언부에 추가해줬다

// App.tsx
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

<Route
  path="quickMenu"
  element={
    <DndProvider backend={HTML5Backend}>
      <QuickMenuList />
    </DndProvider>
  }
/>

3) MenuItem.tsx 컴포넌트 내에 drag and drop 동작 함수 추가해주기

// MenuItem.tsx

import { useDrag, useDrop } from "react-dnd";
import { Identifier, XYCoord } from "dnd-core";

export default function MenuItem() => {
	const ref = useRef<HTMLDivElement>(null);
	
	// 드래그 동작
  const [{ isDragging }, drag] = useDrag({
    type: ItemTypes.MENU, // useDrop의 accept와 일치시켜야 함
    item: { menuNo, index }, // monitor.getItem() 내용으로 들어갈 값 정의
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });
  
  // 드랍 동작
  const [{ handlerId }, drop] = useDrop<TDragItem, void, { handlerId: Identifier | null }>({
    accept: ItemTypes.MENU,
    collect(monitor) {
      return {
        handlerId: monitor.getHandlerId(),
      };
    },
    hover(item, monitor) {
      if (!ref.current) return;

      const dragIndex = item.index;
      const hoverIndex = index;

      if (dragIndex === hoverIndex) return;
      
			const clientOffset = monitor.getClientOffset();
      const hoverBoundingReact = ref.current?.getBoundingClientRect();
      const hoverMiddleY = (hoverBoundingReact.bottom - hoverBoundingReact.top) / 2;
      const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingReact.top;
      
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;

      moveCard(dragIndex, hoverIndex);

      item.index = hoverIndex;
    },
  });
 
	drag(drop(ref));

	return (
		<div ref={ref} data-handler-id={handlerId}>
			...
		</div>
	)
}

const ItemTypes = { MENU: "menu" };

4) 메뉴 순서 변경 이벤트 함수 추가하기

‣ 나는 MenuItem.tsx의 상위 컴포넌트인 MenuList.tsx 에 추가한 후 props로 해당 함수를 내려줬다.

// MenuList.tsx

// 메뉴 순서 변경 이벤트
const moveCard = useCallback((dragIdx: number, hoverIdx: number) => {
  setCards((prevCards: TQuickMenuItem[]) => {
    const newCards = [...prevCards];
    const [draggedCard] = newCards.splice(dragIdx, 1);
    newCards.splice(hoverIdx, 0, draggedCard);

    return newCards.map((card, idx) => ({ ...card, seq: idx + 1 }));
  });
}, []);

5) dnd 동작 실행 결과 화면

‣ 회사 페이지 대신 위 코드로 간단한 예제를 만들어본 결과 !



3. dnd 동작 개선

올해 4월달에 react-dnd 라이브러리를 활용하여 기능 구현을 마친 후 해당 기능을 4개월간 상용에서도 사용하였다.
어드민 페이지라 현재 구현되어 있는 dnd 동작은 충분했지만 약간 거슬리는 라이브러리 특성상 애니메이션 동작 고질병을 해결하지 못했다,,
해결 방법이야 아예 없진 않겠지만 codeSandbox에 다른 사람의 예제 코드도 실행해봤을 때 동일한 애니메이션 동작이 발생함을 확인했다


발생 이슈는 아래와 같다

drop 가능한 영역을 벗어난 곳에 drag하고 있는 아이템을 drop하면 원래 위치로 돌아가는 듯한 애니메이션 동작이 한번 보여진다. 그러나 실제론 정상적으로 drop한 위치와 가까운 drop 가능한 영역에 아이템이 위치하게 된다.
(참고로 해당 이슈는 window 환경에선 발생하지 않고 mac 브라우저 환경에서만 발생한닷)


기능 구현 후 4개월동안 문제없이 사용하고 있긴 했지만 업무가 널널한 틈을 타 해당 dnd 동작을 개선하고자 했다.
이 문제를 해결하려면 라이브러리를 변경하는게 제일 빠르겠다는 생각에 전에 팀원분이 추천해주신 dnd-kit 라이브러리로 변경해보기로 했다.
우선 간단 예제 코드를 구현해보면서 dnd-kit 감을 익혀갔고 추후 구현하고자 하는 기능이 잘 동작한다면 회사 코드에 적용해보기로 !


이전 라이브러리와 비교해보면 최근들어 dnd-kit는 react-beautiful-dnd 와 거의 비슷하게 사용되고 있으며, 다른 dnd 라이브러리와도 큰 차이는 없다


🪡 dnd-kit

우선 dnd-kit 라이브러리에 대한 특징 간단 요약!

  • react dnd 툴 라이브러리
  1. 가볍고 성능 좋음
    : dnd-kit/core 의 경우 10KB 정도 밖에 안 되는 용량이며, 애니메이션 등의 성능도 굿 bb
  2. Sortable의 경우 Preset으로 제공
    : 보다 편하게 마들 수 있도록 Preset된 라이브러리 제공함 (dnd-kit/sortable)
    ⌞ HTML5 Drag and Drop API 로 만들어진 라이브러리가 아님

이제 dnd-kit 라이브러리 사용법을 알아보기 !

1) 우선 dnd-kit 라이브러리 설치

npm install @dnd-kit/core @dnd-kit/sortable

2) Dnd 컴포넌트 추가해주기

‣ 확실히 react-dnd 사용했을 때보다 drag, drop 동작 함수 코드가 간결해졌다 !

import { DndContext, DragEndEvent, DragOverEvent, MouseSensor, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, rectSortingStrategy, SortableContext } from "@dnd-kit/sortable";

export default function MenuDnd() {
  const { setNodeRef } = useDroppable({ id: "menuList" });
  const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 10 } }));
  const menuNoList = items.map((item) => item.menuNo);

  // 메뉴 순서 변경 이벤트
  const moveItem = (active: DragEndEvent["active"], over: DragEndEvent["over"]) => {
    if (!over) return;

    const activeData = active.data.current;
    const overData = over.data.current;

    if (!activeData || !overData) return;

    const activeIndex = activeData.sortable.index;
    const overIndex = overData.sortable.index;

    setItems((items: TQuickMenuItem[]) => {
      const newItems = arrayMove(items, activeIndex, overIndex);
      return newItems.map((item, idx) => ({ ...item, seq: idx + 1 }));
    });
  };

	// 드래그 동작
  const handleDragOver = ({ active, over }: DragOverEvent) => {
    moveItem(active, over);
  };

	// 드랍 동작
  const handleDragEnd = ({ active, over }: DragEndEvent) => {
    if (active.id !== over?.id) {
      moveItem(active, over);
    }
  };

  return (
    <DndContext sensors={sensors} onDragOver={handleDragOver} onDragEnd={handleDragEnd}>
      <SortableContext items={menuNoList} strategy={rectSortingStrategy}>
        <div ref={setNodeRef}>
          {items.map((row, i) => (
            <MenuItem key={row.menuNo} index={i} row={row} setItems={setItems} menuNameArr={menuNameArr} />
          ))}
        </div>
      </SortableContext>
    </DndContext>
  );
}

3) MenuItem.tsx 컴포넌트 추가하기

import { useSortable } from "@dnd-kit/sortable";

export default function MenuItem() {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
    id: row.menuNo,
    data: { index },
  });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    zIndex: isDragging ? 2 : 1,
    boxShadow: isDragging ? `rgba(0, 0, 0, 0.1) 0px 4px 12px` : "none",
    cursor: isDragging ? "grabbing" : "grab",
  };

  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      ...
    </div>
  );
}

4) 예제 결과 화면

‣ drop 가능한 영역을 벗어난 곳에 drop했을 경우 제일 가까운 drop 가능한 영역으로 바로 위치하게 된다. react-dnd 라이브러리처럼 원래 위치로 돌아가는 듯한 애니메이션 동작은 해결 완 ! + dnd 애니메이션도 좀 더 부드럽고 자연스러운 듯 !


5) 회사 코드에 적용

처음 예제 코드 구현할 땐 좀 어려웠는데 한번 구조 익히고 나니까 react-dnd보다 구현하기 훨 간편한 듯 했다.
원래는 메뉴 관리 주기능 로직과 dnd 동작 코드가 한 컴포넌트에서 모조리 구현했었는데 이번에 라이브러리 변경하면서 dnd 로직은 따로 분리하여 코드 가독성면에서도 개선되었다.
사소한 애니메이션 동작 이슈에 어드민 기능이라 딱히 고치지 않아도 되었지만 시간 날 때 사소한 UX 개선하고 상용까지 적용할 수 있다는 건 스타트업이여서 가능한 경험이지 않을까 🤔

profile
FE developer

0개의 댓글