| name | react-dnd |
| description | react-dnd 드래그 앤 드롭 — DndProvider, useDrag/useDrop 훅, TypeScript 타입, 리스트 순서 변경, 커스텀 프리뷰, 중첩 드롭, SSR 주의사항 |
react-dnd 드래그 앤 드롭
소스: https://react-dnd.github.io/react-dnd/docs/overview
소스: https://github.com/react-dnd/react-dnd
검증일: 2026-04-20
설치 및 기본 설정
npm install react-dnd react-dnd-html5-backend
npm install react-dnd-touch-backend
DndProvider 설정
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
function App() {
return (
<DndProvider backend={HTML5Backend}>
<MyDragDropContent />
</DndProvider>
);
}
- DndProvider는 앱 루트에 한 번만 감싼다
- 중첩 DndProvider는 에러를 발생시킨다
- backend prop에 HTML5Backend 또는 TouchBackend를 전달한다
드래그 아이템 타입 정의 (TypeScript)
const ItemTypes = {
CARD: 'card',
COLUMN: 'column',
} as const;
interface DragItem {
type: string;
id: string;
index: number;
}
useDrag 훅
import { useDrag } from 'react-dnd';
function DraggableCard({ id, text, index }: CardProps) {
const [{ isDragging }, dragRef] = useDrag(() => ({
type: ItemTypes.CARD,
item: { id, index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
end: (item, monitor) => {
const dropResult = monitor.getDropResult();
if (item && dropResult) {
}
},
}), [id, index]);
return (
<div ref={dragRef} style={{ opacity: isDragging ? 0.5 : 1 }}>
{text}
</div>
);
}
useDrag spec 주요 옵션
| 옵션 | 타입 | 설명 |
|---|
type | string | 필수. 드래그 아이템 유형 식별자 |
item | object | () => object | 드래그 데이터. 함수면 드래그 시작 시 호출 |
collect | (monitor) => object | monitor 상태를 컴포넌트 props로 매핑 |
end | (item, monitor) => void | 드래그 종료 콜백 |
canDrag | (monitor) => boolean | 드래그 가능 여부 제어 |
isDragging | (monitor) => boolean | 커스텀 isDragging 판별 로직 |
useDrag 반환값
const [collectedProps, dragRef, previewRef] = useDrag(spec, deps);
useDrop 훅
import { useDrop } from 'react-dnd';
function DropZone({ onDrop }: DropZoneProps) {
const [{ isOver, canDrop }, dropRef] = useDrop(() => ({
accept: ItemTypes.CARD,
drop: (item: DragItem) => {
onDrop(item.id);
return { name: 'DropZone' };
},
canDrop: (item: DragItem) => {
return item.id !== 'locked';
},
hover: (item: DragItem, monitor) => {
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}), [onDrop]);
return (
<div
ref={dropRef}
style={{
backgroundColor: && ? '#' '#',
? ' ' ' ',
}}
>
Drop here
);
}
useDrop spec 주요 옵션
| 옵션 | 타입 | 설명 |
|---|
accept | string | string[] | 필수. 수락할 아이템 타입 |
drop | (item, monitor) => object | void | 드롭 시 호출. 반환값은 getDropResult() |
hover | (item, monitor) => void | 아이템이 위에 있을 때 반복 호출 |
canDrop | (item, monitor) => boolean | 드롭 수락 여부 |
collect | (monitor) => object | monitor 상태 수집 |
수락/거부 로직
accept: [ItemTypes.CARD, ItemTypes.COLUMN],
canDrop: (item: DragItem, monitor) => {
return item.id !== currentId && !isLocked;
},
리스트 아이템 순서 변경 패턴
import { useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd';
interface SortableItemProps {
id: string;
index: number;
text: string;
moveItem: (dragIndex: number, hoverIndex: number) => void;
}
function SortableItem({ id, index, text, moveItem }: SortableItemProps) {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, dragRef] = useDrag(() => ({
type: ItemTypes.CARD,
item: { id, index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}), [id, index]);
const [{ isOver }, dropRef] = useDrop(() => ({
accept: ItemTypes.CARD,
hover: (item: DragItem, monitor) => {
(!ref.) ;
dragIndex = item.;
hoverIndex = index;
(dragIndex === hoverIndex) ;
hoverBoundingRect = ref..();
hoverMiddleY = (hoverBoundingRect. - hoverBoundingRect.) / ;
clientOffset = monitor.();
(!clientOffset) ;
hoverClientY = clientOffset. - hoverBoundingRect.;
(dragIndex < hoverIndex && hoverClientY < hoverMiddleY) ;
(dragIndex > hoverIndex && hoverClientY > hoverMiddleY) ;
(dragIndex, hoverIndex);
item. = hoverIndex;
},
: ({
: monitor.(),
}),
}), [index, moveItem]);
((ref));
(
);
}
moveItem 구현 (부모 컴포넌트)
import { useCallback, useState } from 'react';
function SortableList() {
const [items, setItems] = useState([
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
]);
const moveItem = useCallback((dragIndex: number, hoverIndex: number) => {
setItems((prevItems) => {
const next = [...prevItems];
const [removed] = next.splice(dragIndex, 1);
next.splice(hoverIndex, 0, removed);
return next;
});
}, []);
return (
<DndProvider backend={HTML5Backend}>
{items.map((item, index) => (
<SortableItem
key={item.id}
id={item.id}
index={index}
text={item.text}
=
/>
))}
);
}
드래그 미리보기(Preview) 커스터마이징
방법 1: previewRef 사용
const [{ isDragging }, dragRef, previewRef] = useDrag(() => ({
type: ItemTypes.CARD,
item: { id },
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}));
return (
<>
{/* 이 엘리먼트가 드래그 프리뷰로 표시 */}
<div ref={previewRef}>
<CustomPreview />
</div>
{/* 이 엘리먼트를 잡아서 드래그 */}
<div ref={dragRef}>Drag Handle</div>
</>
);
방법 2: useDragLayer로 완전 커스텀 프리뷰
import { useDragLayer } from 'react-dnd';
function CustomDragLayer() {
const { isDragging, item, currentOffset } = useDragLayer((monitor) => ({
item: monitor.getItem(),
isDragging: monitor.isDragging(),
currentOffset: monitor.getSourceClientOffset(),
}));
if (!isDragging || !currentOffset) return null;
return (
<div
style={{
position: 'fixed',
pointerEvents: 'none',
left: 0,
top: 0,
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
zIndex: 9999,
}}
>
<div style={{ background: 'white', padding: 8, boxShadow: '0 2px 8px rgba(0,,,)' }}>
Dragging: {item?.id}
);
}
useDragLayer 사용 시 HTML5Backend 기본 프리뷰를 숨기려면:
import { getEmptyImage } from 'react-dnd-html5-backend';
import { useEffect } from 'react';
const [, dragRef, previewRef] = useDrag(() => ({
type: ItemTypes.CARD,
item: { id },
}));
useEffect(() => {
previewRef(getEmptyImage(), { captureDraggingState: true });
}, [previewRef]);
상세 레퍼런스 (예제·고급 패턴·흔한 실수) → references/REFERENCE.md