--- id: react-dnd-kit-patterns title: dnd-kit — Drag & Drop / Sortable / 키보드 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [react, dnd, drag-drop, vibe-coding] tech_stack: { language: "TS / React", applicable_to: ["Frontend"] } applied_in: [] aliases: [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 ```tsx 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
Drag me
; } function Droppable({ children }: ...) { const { setNodeRef, isOver } = useDroppable({ id: 'zone' }); return
{children}
; } function App() { return ( console.log(e.active.id, '→', e.over?.id)}> Drop here ); } ``` ### Sortable list ```tsx 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
  • {children}
  • ; } function List() { const [items, setItems] = useState(['a', 'b', 'c']); return ( { 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))); } }} > ); } ``` ### Kanban (다중 column) ```tsx 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) ```tsx import { DragOverlay } from '@dnd-kit/core'; const [activeId, setActiveId] = useState(null); setActiveId(e.active.id as string)} onDragEnd={(e) => { setActiveId(null); /* ... */ }} > ... {activeId ? : null} ``` ### Sensor (touch + keyboard) ```tsx 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 }), ); ``` → Tab + Space + Arrow keys 자동. ### Restrict modifier ```tsx import { restrictToVerticalAxis, restrictToParentElement } from '@dnd-kit/modifiers'; ``` ### Server sync (mutation) ```ts 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 로 부드러움. ## 🔗 관련 문서 - [[Frontend_A11y_Testing]] - [[React_Animation_Performance]] - [[React_Virtualization_Lists]]