[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
---
|
||||
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 <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
|
||||
```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 <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)
|
||||
```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<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)
|
||||
```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 }),
|
||||
);
|
||||
|
||||
<DndContext sensors={sensors} ...>
|
||||
```
|
||||
|
||||
→ Tab + Space + Arrow keys 자동.
|
||||
|
||||
### Restrict modifier
|
||||
```tsx
|
||||
import { restrictToVerticalAxis, restrictToParentElement } from '@dnd-kit/modifiers';
|
||||
|
||||
<DndContext modifiers={[restrictToVerticalAxis, restrictToParentElement]} ...>
|
||||
```
|
||||
|
||||
### 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]]
|
||||
Reference in New Issue
Block a user