Files
2nd/10_Wiki/Topics/Coding/React_DnD_Kit_Patterns.md
T
2026-05-09 21:08:02 +09:00

5.9 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
react-dnd-kit-patterns dnd-kit — Drag & Drop / Sortable / 키보드 Coding draft B conceptual 2026-05-09 2026-05-09
react
dnd
drag-drop
vibe-coding
language applicable_to
TS / React
Frontend
dnd-kit
drag and drop
sortable
kanban
keyboard accessibility

dnd-kit

Modern React drag & drop. A11y (키보드) 일급, 가벼움, 모바일 OK. Sortable list / Kanban / Tree. react-dnd 보다 가볍고 modern.

📖 핵심 개념

  • DndContext: provider.
  • useDraggable / useDroppable: 기본 hook.
  • SortableContext + useSortable: sortable list 자동.
  • Sensor: pointer / keyboard / touch.

💻 코드 패턴

단순 drag/drop

import { DndContext, useDraggable, useDroppable } from '@dnd-kit/core';

function Draggable() {
  const { attributes, listeners, setNodeRef, transform } = useDraggable({ id: 'a' });
  const style = transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` } : undefined;
  return <div ref={setNodeRef} style={style} {...attributes} {...listeners}>Drag me</div>;
}

function Droppable({ children }: ...) {
  const { setNodeRef, isOver } = useDroppable({ id: 'zone' });
  return <div ref={setNodeRef} style={{ background: isOver ? '#eef' : '#fff' }}>{children}</div>;
}

function App() {
  return (
    <DndContext onDragEnd={(e) => console.log(e.active.id, '→', e.over?.id)}>
      <Draggable />
      <Droppable>Drop here</Droppable>
    </DndContext>
  );
}

Sortable list

import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, useSortable, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

function SortableItem({ id, children }: ...) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
  const style = { transform: CSS.Transform.toString(transform), transition };
  return <li ref={setNodeRef} style={style} {...attributes} {...listeners}>{children}</li>;
}

function List() {
  const [items, setItems] = useState(['a', 'b', 'c']);

  return (
    <DndContext
      collisionDetection={closestCenter}
      onDragEnd={(e) => {
        if (e.over && e.active.id !== e.over.id) {
          setItems((it) => arrayMove(it, it.indexOf(e.active.id as string), it.indexOf(e.over!.id as string)));
        }
      }}
    >
      <SortableContext items={items} strategy={verticalListSortingStrategy}>
        <ul>{items.map(id => <SortableItem key={id} id={id}>{id}</SortableItem>)}</ul>
      </SortableContext>
    </DndContext>
  );
}

Kanban (다중 column)

const [columns, setColumns] = useState({
  todo: ['Task 1', 'Task 2'],
  doing: ['Task 3'],
  done: [],
});

function onDragEnd(e: DragEndEvent) {
  const { active, over } = e;
  if (!over) return;
  const activeCol = findCol(columns, active.id);
  const overCol = findCol(columns, over.id) ?? over.id; // column 자체에 drop 가능
  if (activeCol === overCol) {
    setColumns(c => ({ ...c, [activeCol]: arrayMove(c[activeCol], 
        c[activeCol].indexOf(active.id as string), 
        c[overCol].indexOf(over.id as string)) }));
  } else {
    setColumns(c => {
      const fromIdx = c[activeCol].indexOf(active.id as string);
      const toIdx = over.id === overCol ? c[overCol].length : c[overCol].indexOf(over.id as string);
      const item = c[activeCol][fromIdx];
      return {
        ...c,
        [activeCol]: c[activeCol].filter((_, i) => i !== fromIdx),
        [overCol]: [...c[overCol].slice(0, toIdx), item, ...c[overCol].slice(toIdx)],
      };
    });
  }
}

Drag overlay (drag 중 ghost)

import { DragOverlay } from '@dnd-kit/core';

const [activeId, setActiveId] = useState<string | null>(null);

<DndContext
  onDragStart={(e) => setActiveId(e.active.id as string)}
  onDragEnd={(e) => { setActiveId(null); /* ... */ }}
>
  <SortableContext items={items}>...</SortableContext>
  <DragOverlay>
    {activeId ? <ItemPreview id={activeId} /> : null}
  </DragOverlay>
</DndContext>

Sensor (touch + keyboard)

import { useSensors, useSensor, PointerSensor, KeyboardSensor } from '@dnd-kit/core';
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';

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

<DndContext sensors={sensors} ...>

→ Tab + Space + Arrow keys 자동.

Restrict modifier

import { restrictToVerticalAxis, restrictToParentElement } from '@dnd-kit/modifiers';

<DndContext modifiers={[restrictToVerticalAxis, restrictToParentElement]} ...>

Server sync (mutation)

function onDragEnd(e: DragEndEvent) {
  setItems(...); // optimistic
  api.reorder.mutate({ ids: newOrder })
    .catch(() => setItems(prev)); // 실패 시 복원
}

🤔 의사결정 기준

상황 추천
Sortable list dnd-kit + SortableContext
Kanban dnd-kit multi-container
Tree dnd-kit tree (커뮤니티)
Native HTML5 DnD only 직접 (간단 기능)
큰 list (수천) virtualized + dnd-kit
Native mobile RN reanimated + gesture handler

안티패턴

  • Sensor 없이 mobile: touch 안 작동.
  • A11y 무시 (키보드): 사용자 일부 못 씀.
  • DragOverlay 없음 — sortable 시 lag: jumpy. overlay 권장.
  • State update 동기 X: 즉시 update + 서버 비동기.
  • Server reorder 후 refetch — 깜빡임: optimistic + rollback.
  • id 가 unstable: 매 render 새 id — drag 깨짐.

🤖 LLM 활용 힌트

  • DndContext + SortableContext + useSortable 3종.
  • Sensor 항상 포함 (Pointer + Keyboard).
  • DragOverlay 로 부드러움.

🔗 관련 문서