[@dnd-kit/Sortable] 사용해서 component 순서 바꾸기

해달·2024년 2월 3일
post-thumbnail

어드민에서 관리자가 드래그를 여러 컴포넌트를 오고가며 순서가 굉장히 유동적으로 바뀌는 기능일 작업하게 되었다.

dnd를 쓸까 했지만, dnd-kit에 원하는 공식예제가 있는 걸 보고 사용하기로 했다.


사용한 버전

"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",

요구사항

  • 하나의 스크롤링 되는 아이템에 여러개의 하위 아이템이 있다.
  • 이 하위 아이템은 추가/삭제 되어서 늘어날 수 있다.

공식 예제

여기서 드래그앤 드롭 이용했고,

https://docs.dndkit.com/presets/sortable

공식문서에 가면 Item, Context를 이용한 간단한 예시가 있다.


📌 dnd-kit을 활용한 드래그 앤 드롭 구현

리스트 아이템을 드래그로 정렬하는 기능을 구현한 예제
관리자가 여러 개의 하위 아이템을 추가/삭제하고, 유동적으로 순서를 변경할 수 있도록 설계되었다.

💡 코드 분석 및 구현 방식

1️⃣ 드래그 센서 설정

const sensors = useSensors(
  useSensor(PointerSensor),
  useSensor(KeyboardSensor, {
    coordinateGetter: sortableKeyboardCoordinates,
  })
);
  • useSensors로 드래그 입력을 감지하는 센서를 설정
  • PointerSensor → 마우스, 터치 이벤트 감지
  • KeyboardSensor → 키보드 화살표 키로 이동 가능하게 설정
  • sortableKeyboardCoordinates → 키보드 이동 좌표 계산

2️⃣ 드래그 종료 후 아이템 위치 변경 (onDragEnd)

const onDragEnd = (event) => {
  const { active, over } = event;

  if (!over) return; // 드래그 도중 놓칠 경우 예외 처리

  if (active.id !== over.id) {
    setItems((items) => {
      const oldIndex = items.findIndex((item) => item.id === active.id);
      const newIndex = items.findIndex((item) => item.id === over.id);

      return arrayMove(items, oldIndex, newIndex);
    });
  }
};
  • onDragEnd는 dnd-kit에서 드래그가 끝났을 때 실행되는 콜백 함수
  • active.id: 드래그한 아이템
  • over.id: 드롭한 위치의 아이템
  • arrayMove(items, oldIndex, newIndex):
    dnd-kit에서 제공하는 배열 내 아이템을 이동하는 유틸 함수
    → 기존 배열을 직접 변경하지 않고, 새로운 배열을 반환하여 상태 업데이트

3️⃣ DndContext와 SortableContext 설정

<DndContext
  sensors={sensors}
  collisionDetection={closestCenter}
  onDragEnd={onDragEnd}
>
  <SortableContext items={items} strategy={verticalListSortingStrategy}>
    {items.map((component) => (
      <SortableItem id={component.id} key={component.id}>
        {renderComponent(component.childList, component.category)}
      </SortableItem>
    ))}
  </SortableContext>
</DndContext>

4️⃣ 드래그 가능한 개별 아이템 (SortableItem)

const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });

return (
	<div ref={setNodeRef} {...attributes} {...listeners}>
	  <Icon.Group />
	</div>
  )

위 내용을 기반으로 내가 적용한 코드

Component.tsx

import {
	DndContext,
	KeyboardSensor,
	PointerSensor,
	closestCenter,
	useSensor,
	useSensors,
} from '@dnd-kit/core';
import {
	SortableContext,
	arrayMove,
	sortableKeyboardCoordinates,
	verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import React, { useState } from 'react';
import { SortableItem } from '../../common/SortableItem';
import { Divider } from './child/divider/Divider';

export const Component = () => {
	const [items, setItems] = useState([]);

	const sensors = useSensors(
		useSensor(PointerSensor),
		useSensor(KeyboardSensor, {
			coordinateGetter: sortableKeyboardCoordinates,
		}),
	);

	const onDragEnd = (event) => {
		const { active, over } = event;

		if (!over) return;

		if (active.id !== over.id) {
			setItems((items) => {
				const oldIndex = items.findIndex((item) => item.id === active.id);
				const newIndex = items.findIndex((item) => item.id === over.id);

				return arrayMove(items, oldIndex, newIndex);
			});
		}
	};

	const renderComponent = (
		category: 'div' | 'divider',
		componentId: number,
	) => {
		return {
			div: <div />,
			divider: <Divider />,
		}[category];
	};

	return (
		<DndContext
			sensors={sensors}
			collisionDetection={closestCenter}
			onDragEnd={onDragEnd}>
			<SortableContext items={items} strategy={verticalListSortingStrategy}>
				<>
					{items.map((component) => {
						return (
							<SortableItem id={component.id} key={component.id}>
								<React.Fragment key={component.id}>
									{renderComponent(component.childList, component.category)}
								</React.Fragment>
							</SortableItem>
						);
					})}
				</>
			</SortableContext>
		</DndContext>
	);
};

SortableItem.tsx

import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Icon } from 'images/icon';
import { PropsWithChildren } from 'react';

interface Props extends PropsWithChildren {
	id: string | number;
}

export function SortableItem(props: Props) {
	const { id, children } = props;
	const {
		attributes,
		listeners,
		setNodeRef,
		transform,
		transition,
	} = useSortable({ id });

	const style = {
		transform: CSS.Transform.toString(transform),
		transition,
		cursor: 'grab',
		width: '100%',
		display: 'flex',
		gap: '8px',
	};

	return (
		<div style={style}>
        /* 클릭 할 아이콘 */
			<div ref={setNodeRef} {...attributes} {...listeners}>
				<Icon.Group /> 
			</div>
			{children}
		</div>
	);
}

위와 같이 적용하면 내가 원하는 아이콘을 잡고 적용되어있는 하위 컴포넌트들을 동작시킬 수 있다!

중간에 삭제한 로직은 많지만 원하는 내용이 많아 커스텀해야 할 부분이 많다면
이 라이브러리를 써보는것도 좋을 것 같다


reference

0개의 댓글