문서/시작하기

v2에서 마이그레이션

v2 앱을 v3로 옮깁니다. 무엇이 왜 바뀌었는지, 업그레이드 방법, 모든 v2 API와 그 v3 대체 항목의 대응표, 흔한 경우의 전후 코드, 그리고 주의할 점.

이 페이지 편집

이 페이지는 AI가 번역했으며 부정확한 내용이 있을 수 있습니다. 영어 원문 읽기

v3는 v2를 새로 작성한 것입니다. 모델은 그대로입니다. 프로바이더 하나와, 직접 만든 요소에 호출하는 컴포저블입니다. 하지만 대부분의 이름과 옵션이 바뀌었고, 정렬은 라이브러리 안으로 들어왔으며, 리스트마다 작성하던 드롭 핸들러는 사라집니다. 이 페이지는 v2의 각 부분을 v3에 대응시킨 뒤, 흔한 경우를 전후로 보여 줍니다.

바뀐 점

  • 정렬이 기본으로 들어 있습니다. v2에서 리스트는 드롭 영역이었고, 그 행은 [index, items] 페이로드를 가진 draggable이었으며, onDrop에서 e.helpers.suggestSort()로 정렬했습니다. v3에서는 makeSortableList와 makeSortableItem이 이를 처리합니다. 리스트는 원본 배열의 초안을 렌더링하고, 초안은 드래그하는 동안 순서가 바뀌며, 드롭하면 새 배열이 ref에 기록됩니다. v2의 드롭 핸들러는 대부분 옮기는 것이 아니라 삭제합니다.
  • 알아서 움직입니다. 행은 새 자리로 미끄러지고(FLIP), 오버레이는 드롭하면 자기 자리에 안착하며 취소하면 원래 자리로 돌아갑니다. <TransitionGroup>은 필요 없습니다.
  • 여전히 패키지 하나입니다. @vue-dnd-kit/core에 리스트, 트리, 멀티 드래그, 모디파이어, 센서, 충돌 감지 함수가 들어 있습니다. Nuxt 모듈은 그대로이며, @vue-dnd-kit/actions(프로그래밍 방식 드래그)와 @vue-dnd-kit/devtools는 새로 생긴 선택 패키지입니다.
  • 진짜 오버레이. body로 텔레포트되고, 안착할 자리의 크기에 맞춘 깊은 클론을 보여 줍니다. 오버레이 컴포넌트는 프로바이더 내부를 읽는 대신 드래그를 props로 받습니다.
  • 요소가 아니라 id. 선택, 이벤트, 드래그 상태는 데이터의 id를 key로 씁니다. 읽어야 할 DOM 요소 맵은 없습니다.
  • 타입과 accept 규칙이 일반 draggable과 드롭 대상의 groups를 대신합니다. 리스트는 group 이름으로 항목을 주고받습니다.
  • 드롭 전에 결정합니다. onBeforeDrop이 받을지, 거부할지, 드래그를 계속할지, 항목 일부만 받을지 정합니다. onDrop은 적용만 합니다.
  • 센서. 포인터와 키보드 입력은 센서입니다. 활성화는 포인터 종류별로 정하고, 터치 드래그는 길게 누르기로 시작하므로 손가락 아래에서도 리스트가 계속 스크롤됩니다.
  • 영역. makeDragArea가 makeConstraintArea와 makeAutoScroll을 대신하며, 자동 스크롤은 코드 없이 모든 스크롤 컨테이너에서 동작합니다.
  • 접근성이 기본으로 켜져 있습니다. ARIA 속성, 드롭 후 돌아오는 포커스, 스크린 리더 안내 문구.

업그레이드

npm i @vue-dnd-kit/core@^3
패키지v2v3
@vue-dnd-kit/core2.4.x3.x
@vue-dnd-kit/nuxt1.x3.x
@vue-dnd-kit/actions—1.x, 선택
@vue-dnd-kit/devtools—3.x, 선택
  • package.json에 "@vue-dnd-kit/core": "^2…"가 있는 앱은 범위가 ^3이 될 때까지 v2에 머뭅니다.
  • Vue 3.5 이상. v2에는 의존성이 없었고, v3에는 @vueuse/core(14 또는 15) 하나가 있습니다.
  • 브라우저: Chrome과 Edge 104+, Safari 15.4+, Firefox 104+.
  • v2와 v3는 패키지 이름이 같으므로, 앱은 한 번에 v3로 옮겨 갑니다. 호환 레이어는 없습니다. v2 문서는 페이지 위쪽의 버전 메뉴에서 계속 볼 수 있습니다.

권장하는 작업 순서:

  1. 패키지를 업그레이드하고 프로바이더의 props 이름을 바꾸세요(아래).
  2. 정렬 가능한 리스트를 각각 makeSortableList와 makeSortableItem으로 바꾸고, 그 드롭 핸들러를 삭제하세요.
  3. 일반 draggable과 드롭 영역을 옮기세요.
  4. 커스텀 미리보기를 오버레이 컴포넌트나 프로바이더의 #overlay 슬롯으로 옮기세요.
  5. makeConstraintArea와 makeAutoScroll을 makeDragArea로 바꾸거나, 아무것도 쓰지 마세요.
  6. v2를 위한 우회책을 삭제하세요. draggable의 tabindex와 aria-grabbed, 항목 전체의 touch-action: none, 행을 감싼 <TransitionGroup>, 미리보기 컴포넌트를 감싼 markRaw.

이전과 이후

표의 대응 없음은 v3에 그런 이름이 없다는 뜻입니다. 대신 무엇을 쓸지는 셀에 적혀 있습니다.

한눈에 보기

v2v3
DnDProviderDnDProvider. 새 props와 #overlay 슬롯이 있습니다.
makeDraggable자유로운 드래그에는 makeDraggable, 리스트의 행에는 makeSortableItem.
makeDroppable일반 드롭 대상에는 makeDroppable, 리스트와 트리에는 makeSortableList 또는 makeSortableTree.
makeSelectionAreamakeSelectionArea.
makeConstraintAreamakeDragArea.
makeAutoScroll필요 없음: 자동 스크롤은 자동입니다. makeDragArea나 리스트의 autoScroll로 조정합니다.
useDnDProvider상태는 useDnD, 이벤트는 useDnDMonitor.
injectionKeyuseDnDContext()와 provideDnDContext(app, context).
DragPreviewDragPreview: 이제 드래그 중인 항목 하나의 클론이며, 직접 만드는 오버레이에 씁니다.
makeSnappedOverlayPosition, <DragPreview :grid>snapToGrid 모디파이어.
e.helpers.suggestSort, suggestSwap, suggestCopy, suggestRemove리스트에 기본으로 들어 있습니다. 정렬을 참고하세요.

프로바이더

v2v3
autoScrollViewport, 설정하지 않으면 꺼짐autoScroll: 기본으로 켜짐, 창과 모든 스크롤 컨테이너에 적용됩니다. { threshold, maxSpeed, acceleration } 또는 false.
previewTo, 기본으로 프로바이더 안에 렌더링overlayTo: 선택자나 요소, 기본값은 'body'.
#preview="{ preview }" 슬롯드래그를 props로 받는 #overlay 슬롯: items, active, over, accepted, status, mode, to. 오버레이를 참고하세요.
keyboard.keys.forDrag, forDrop, forCancel(useDnDProvider()에서 설정):keyboard="{ keys: { start, drop, cancel } }".
keyboard.step:keyboard="{ step }": 기본값 25 px, 자유로운 드래그용.
keyboard.keys.forMove, forMoveFaster, keyboard.moveFaster대응 없음. 화살표 키로 움직이고, Shift를 누르면 자유로운 드래그의 이동 단위가 네 배가 됩니다.
collision.throttle대응 없음. collision이 대상을 찾는 방법을 고릅니다. pointerFirst(기본값), pointerWithin, rectIntersection, closestCenter, closestCorners, 또는 직접 만든 함수.
페이지마다 프로바이더 하나, 중첩 불가중첩할 수 있습니다. 항목은 가장 가까운 프로바이더가 관리합니다. 앱 전체에 프로바이더 하나가 여전히 목표입니다.

v2에 대응하는 것이 없는 새 props: activation, modifiers, sensors, animation, dropAnimation, animateChanges, haptics, copyModifier, overlayFit, overlayClass, overlayTransition, overlayPreviews, announcements, screenReaderInstructions, defaultStyles. 프로바이더는 drag-start, drag-move, drag-over, drag-end, drag-cancel, drag-resume, drag-settle도 내보냅니다. 모두 프로바이더 가이드에 있습니다.

상태 읽기: useDnD

useDnDProvider()v3
state: 'dragging', 'activating', 'selecting', 'pending' 또는 nulluseDnD().status: 'idle', 'activating', 'dragging', 'dropping' 또는 'selecting'. 'pending'(비동기 드롭)은 'dropping'이고, null은 'idle'입니다.
pointer.value.currentuseDnD().pointer, 모디파이어를 적용한 값. 모든 이벤트에는 시작점으로부터의 오프셋인 delta도 있습니다.
entities.draggingMapuseDnD().active: id, ids, items, data, type, element, rect.
entities.selectedSet(요소)useDnD().selection(id): has, add, delete, toggle, set, clear, ids.
hovered.droppable, hovered.draggableuseDnD().over: id, data, element, rect, accepted.
delay.progress, distanceProgressuseDnD().activation.progress, 0부터 1까지.
preview.position, preview.size, preview.render, preview.to대응 없음. 오버레이의 위치와 크기는 라이브러리가 정합니다. overlayTo, #overlay 슬롯, 또는 오버레이 컴포넌트를 쓰세요.
entities.draggableMap, droppableMap, visible…Set들, scrollPosition대응 없음.
—새로 추가: useDnD().cancel(), position, isAutoScrolling.

이벤트는 useDnDMonitor로 옮겨 갔습니다. 이벤트와 드롭 결정을 참고하세요. 전체 구조는 useDnD API에 있습니다.

makeDraggable

v2v3
makeDraggable(ref, options, payload)makeDraggable(ref, options): 세 번째 인자는 없습니다. 리스트의 행은 makeSortableItem(ref, { item })을 씁니다.
id, 가상 리스트에서 필요id: 데이터에서 getter로 넘기세요. 기본값은 useId()에서 오며 컴포넌트 인스턴스마다 새로 만들어집니다.
data: () => carddata: 값, ref, 또는 getter. active.data와 active.items[i].data로 다시 읽습니다.
groups: ['dev']type: 'dev', 대상의 accept와 비교됩니다. 그룹을 참고하세요.
dragHandle: '.handle'handle: 선택자나 요소 ref.
disableddisabled.
modifier: { keys, method }canDrag: (event) => event.altKey: 누를 때마다 확인하는 조건.
activation: { distance, delay }포인터 종류별 activation: { mouse: { distance }, touch: { delay, tolerance }, pen }. distance와 delay를 둘 다 주면 먼저 충족되는 쪽에서 드래그가 시작됩니다.
activation.distance: { x, y, condition }대응 없음. 포인터 종류마다 distance 하나.
render: markRaw(Preview)overlay: Preview: 컴포넌트가 드래그를 props로 받습니다. 오버레이를 참고하세요.
placementMargins대응 없음. 정렬 가능한 항목 위의 드롭 대상은 그 가운데(25~75%)에서 판정됩니다. 트리는 nesting: 'hover'로 중첩합니다. 정렬 가능한 항목 안으로 드롭을 참고하세요.
events.onSelfDragStart, onSelfDragMove, onSelfDragEnd, onSelfDragCanceldraggable에는 대응 없음: useDnDMonitor나 watch(isDragging)을 쓰세요. 이벤트를 참고하세요.
events.onHover, onLeave, 그리고 그 위를 지나는 드래그에 대한 onDragStart…같은 요소에 makeDroppable: onDragEnter, onDragLeave.
isDragging, selected 반환같습니다.
isDragOver 반환정렬 가능한 항목은 indicator(placeholder: 'indicator'일 때)와 isPlaceholder를 반환하고, 드롭 대상은 placement를 반환합니다.
isAllowed 반환draggable에는 대응 없음. 드롭 대상은 isAccepting을 반환합니다.
—새로 추가: label, axis, modifiers, keyboardMode, 반환된 transform과 함께 쓰는 overlay: false, dropAnimation, attributes, native.

makeDroppable

v2v3
makeDroppable(ref, options, () => items)makeDroppable(ref, options). 리스트를 담은 영역은 makeSortableList입니다.
groupsaccept: '*', 타입 하나, 타입 목록, 또는 함수.
groupMatch: 'every', 'some'acceptMode: 'all', 'some'.
events.onEnter, events.onLeaveonDragEnter, onDragLeave: 각각 독립된 옵션이며 events 객체는 없습니다.
false나 Promise를 반환할 수 있던 events.onDroponBeforeDrop이 결정하고 onDrop이 적용합니다. 드롭 결정을 참고하세요.
events.onValidate남길 id를 반환하는 onBeforeDrop.
datadata, over.data로 다시 읽습니다.
disableddisabled.
isAllowed 반환isAccepting.
isDragOver 반환: { top, right, bottom, left, center }isOver(불리언)와 placement: { top, right, bottom, left }, 포인터가 있는 쪽 절반. center는 없습니다.
—새로 추가: id, label, priority, overOverlay, overDropAnimation, external(컴퓨터의 파일).

드롭 이벤트

v2 IDragEventv3
e.draggedItemse.active.items: 각각 { id, item, data, label, type }이며, 집은 항목이 맨 앞입니다. 리스트는 e.items와 e.keys도 받습니다.
e.draggedItems[0].datae.active.data.
e.draggedItems[i].index, .items리스트: e.from, { list, index }.
e.dropZone: items, placement, datae.over: id, data, element, rect, accepted. 리스트: e.to, { list, index }.
e.hoveredDraggable대응 없음: 리스트가 e.to.index를 직접 계산합니다.
e.helpers.suggestSort()리스트가 드롭을 기록합니다. e.result에는 드롭이 바꾸는 모든 리스트의 새 배열이 들어 있습니다.
e.helpers.suggestSwap()리스트에 behavior: 'swap'. e.partner는 자리를 맞바꾼 항목입니다.
e.helpers.suggestCopy()group: { name, pull: 'clone' } 또는 복사 키(Alt)와 리스트의 clone. e.mode는 'copy'입니다.
e.helpers.suggestRemove()드롭 대상에서 e.source.remove(), 또는 리스트의 dropOutside: 'remove'.
result.sameListe.sameList.
e.helpers.insertAt, removeAt, removeIndexes, swapAt, getInsertSide, isAtZoneStart, isAtZoneEnd대응 없음. 리스트에는 필요 없고, 일반 드롭 대상에서는 데이터를 원하는 대로 바꾸세요.
e.provideruseDnD().
—새로 추가: e.pointer, e.delta, e.position(보드에 드롭한 항목을 놓을 위치), e.mode.

정렬

v2v3
() => items를 넘긴 makeDroppable 리스트makeSortableList(listRef, { items }).
() => [index, items]를 넘긴 makeDraggable 행makeSortableItem(el, { item: () => props.task }).
원본 배열을 렌더링반환된 items를 렌더링: 드래그하는 동안 순서가 바뀌는 초안입니다.
suggestSort('vertical'), suggestSort('horizontal')strategy: 'vertical'(기본값), 'horizontal' 또는 'grid'.
참조 비교로 출발 배열과 도착 배열을 찾은 뒤 sourceItems와 targetItems를 대입필요 없음: 드롭이 바뀌는 모든 리스트에 기록합니다.
<TransitionGroup>기본으로 들어 있습니다. animateChanges: true는 드래그가 아닌 변경(추가, 삭제, 섞기)을 애니메이션합니다.
isDragOver.top, .bottom으로 그리는 삽입선placeholder: 'indicator'와 항목의 indicator.side. 삽입선을 참고하세요.
suggestSwap()behavior: 'swap'.
복사 전용 팔레트(suggestCopy()와 copyOnly 플래그)group: { name, pull: 'clone', put: false }, sort: false, clone.
suggestRemove()를 쓰는 휴지통 영역makeDroppable(trash, { onDrop: (e) => e.source?.remove() }), 또는 dropOutside: 'remove'.
Pinia나 불변 데이터: 결과를 직접 대입items를 getter로 두고, onDrop에서 e.preventDefault()를 호출한 뒤 e.result를 적용합니다. 제어 리스트를 참고하세요.
재귀 트리: 폴더마다 droppable 하나와 applyToTreemakeSortableTree: 컨테이너 하나, 행은 깊이와 함께 평평하게 렌더링됩니다. 트리를 참고하세요.
v-model을 쓰는 중첩 그룹(그룹마다 droppable 하나)리스트 안의 리스트. 모두 같은 group에 두고, 각각 items: defineModel().
가상 리스트: 모든 draggable에 id초안을 가상화 도구에 넘기고, 행에 id: () => props.item.id. 가상 리스트를 참고하세요.

전후 코드는 아래에 있습니다. 리스트에 관한 모든 것은 정렬 가능한 리스트와 항목을 공유하는 리스트에 있습니다.

그룹

v2에는 모든 것에 쓰는 groups 옵션 하나가 있었습니다. v3는 이를 둘로 나눕니다.

v2v3
makeDraggable(el, { groups: ['dev'] })makeDraggable(el, { type: 'dev' }).
makeDroppable(el, { groups: ['dev'] })makeDroppable(el, { accept: 'dev' }).
영역의 groups: ['dev', 'ops']accept: ['dev', 'ops'], 또는 함수: ({ active, items, types }) => boolean.
groups: [], 또는 지정 안 함type이나 accept를 생략합니다. v2의 규칙은 그대로입니다. 타입이 없는 draggable은 어떤 규칙에도 맞고, accept가 없는 대상은 모든 것을 받습니다.
groupMatch: 'every', 'some'acceptMode: 'all', 'some'.
리스트 영역과 그 행의 groupsmakeSortableList의 group: 'board'. group: { name, pull, put }은 무엇이 나가고 들어올 수 있는지 정합니다.
makeSelectionArea의 groupsaccept.

group이 없는 리스트는 그 자체로 하나의 그룹이며 다른 어떤 리스트와도 항목을 주고받지 않습니다. v2에서는 groups가 없는 영역이 모든 것을 받았습니다. 대상이 받는 드래그를 참고하세요.

오버레이

v2v3
프로바이더 안에 렌더링되고 previewTo로 텔레포트되는 미리보기항상 텔레포트됩니다. body 또는 overlayTo로.
클론: 같은 태그, 그 innerHTML, 그 크기깊은 클론: 폼 값, <canvas> 픽셀, 상속된 스타일, 테이블 셀도 함께 옵니다. 남겨 둘 것은 data-dnd-no-clone으로 표시하세요.
useDnDProvider().entities를 읽는 render: markRaw(Preview)items, active, over, accepted, status, mode, to를 props로 받는 overlay: Preview. markRaw는 필요 없으며, 바꿔 끼우려면 shallowRef를 쓰세요.
preview.position으로 위치를 정하는 #preview 슬롯#overlay 슬롯: 박스의 위치와 크기는 라이브러리가 정합니다. 아무것도 렌더링하지 않으면 클론을 씁니다.
useDnDProvider().preview.render.value = …#overlay 슬롯, 또는 overlay로 넘긴 shallowRef.
onEnter와 onLeave로 바꾸는, 영역 위에서의 새 모습makeDroppable이나 리스트의 overOverlay, 또는 오버레이 컴포넌트에서 over를 읽으세요. 오버레이를 바꾸는 대상을 참고하세요.
<DragPreview v-slot="{ draggingMap }">오버레이 props의 items. <DragPreview :id="item.id" />는 클론 하나를 렌더링합니다.
<DragPreview :grid>, makeSnappedOverlayPositionsnapToGrid 모디파이어. 그림만이 아니라 대상을 찾는 위치와 드롭의 position도 스냅합니다.
DragPreview를 감싼 <Transition>이나 motion의 AnimatePresence집을 때는 @starting-style을 쓴 overlay-class, 안착에는 dropAnimation, 컴포넌트 사이에는 overlayTransition. 들어 올리고, 기울이고, 내려놓기를 참고하세요.
.dnd-kit-preview박스의 [data-dnd-overlay]("dragging", "dropping"), 그 안의 [data-dnd-overlay-content].
직접 갱신하던 preview.size박스가 안착할 자리의 크기를 따릅니다(overlayFit).
드롭하면 미리보기가 사라짐자기 자리에 안착하거나, 취소하면 원래 자리로 돌아갑니다.

자세한 내용은 오버레이 가이드에 있습니다.

선택과 멀티 드래그

v2v3
makeDraggable이 반환하는 selectedmakeDraggable과 makeSortableItem이 반환하는 selected.
entities.selectedSet, 요소의 집합useDnD().selection, id가 key.
makeSelectionArea: 기본으로 Ctrl(ControlLeft)을 누른 채기본으로 키가 필요 없습니다. 영역의 빈 공간을 누르면 사각형을 그립니다. modifier: { keys: ['Control'], method: 'every' }로 Ctrl을 되살릴 수 있습니다.
KeyboardEvent.code로 쓰는 modifier.keysKeyboardEvent.key('Control', 'Shift') 또는 .code('ControlLeft').
strategy: 'toggle', 'select'같습니다.
groupsaccept.
events.onSelected(elements)onSelected(ids).
disabled같습니다.
선택을 한꺼번에 옮기는 suggestSort()리스트가 옮깁니다. multiple: 'group'(기본값), 'gaps' 또는 'stack'이 다른 항목이 도중에 어떻게 할지 정합니다.
섞인 선택을 위한 onValidateacceptMode: 'some'과 id를 반환하는 onBeforeDrop. 일부만 받기를 참고하세요.

자세한 내용은 멀티 드래그와 선택에 있습니다.

축, 경계, 스냅

v2v3
makeConstraintArea(el, { axis: 'x' })makeDragArea(el, { axis: 'x' }), 그 안에서 시작하는 드래그에 적용됩니다. axis는 draggable, 리스트, 트리, 항목에도 쓸 수 있습니다.
axis: 'both'axis를 생략합니다.
restrictToArea: truemakeDragArea(el, { restrictToArea: true }), 또는 모디파이어 restrictToParent와 restrictToElement(el).
규칙을 나누기 위한 구역별 프로바이더프로바이더 하나와 구역마다 makeDragArea. 그 항목은 여전히 어디로든 움직일 수 있습니다.
<DragPreview :grid="20">, makeSnappedOverlayPositionsnapToGrid(20) 또는 snapToGrid({ x, y, origin, smooth })를 프로바이더, 영역, 리스트, draggable의 modifiers에.
—새로 추가: restrictToWindowEdges, restrictToFirstScrollableAncestor, snapCenterToCursor, 그리고 직접 만든 모디파이어.

자세한 내용은 축, 경계, 그리드에 있습니다.

자동 스크롤

v2v3
<DnDProvider :autoScrollViewport="true">, 기본으로 꺼짐기본으로 켜짐, 창과 모든 스크롤 컨테이너에 적용됩니다. :auto-scroll="false"로 끕니다.
makeAutoScroll(el, { threshold, speed })필요 없음: overflow: auto나 scroll인 요소는 모두 스크롤됩니다. 하나를 조정하려면 makeDragArea(el, { autoScroll: { … } }) 또는 리스트의 autoScroll.
px 단위, 또는 변마다 정하는 thresholdthreshold: 컨테이너 크기에 대한 비율(0.2), 24~96 px 사이. 변마다 정하는 임계값은 대응 없음.
배수인 speedmaxSpeed(프레임당 25 px)와 acceleration(10).
disabled: trueautoScroll: false.
isScrollingmakeDragArea나 useDnD()의 isAutoScrolling.

키보드와 접근성

v2v3
모든 draggable의 tabindex="0"자동으로 설정되며 role, aria-roledescription, aria-describedby, aria-disabled, aria-pressed도 붙습니다. 직접 지정한 속성이 우선합니다.
직접 쓴 :aria-grabbed="isDragging"지우세요. aria-pressed가 드래그를 따라갑니다.
useDnDProvider().keyboard.keys에 지정한 키프로바이더의 keyboard prop. 바인딩은 event.code나 event.key와 비교됩니다.
화살표 키가 미리보기를 keyboard.step만큼 이동자유로운 드래그: keyboard.step px씩. 리스트: 자리 단위로, 그리고 그룹의 다음 리스트로. keyboardMode: 'droppables'는 대상 사이를 건너뜁니다.
화살표 키 다시 지정(forMove, forMoveFaster)대응 없음.
안내 문구 없음라이브 리전이 집기, 이동, 드롭, 취소를 모두 안내합니다. label이 이름을 붙이고, announcements와 screenReaderInstructions가 문구를 바꿉니다.

자세한 내용은 접근성에 있습니다.

이벤트와 드롭 결정

v2v3
draggable의 events.onSelfDragStartuseDnDMonitor({ onDragStart })에서 active.ids를 확인합니다. 클래스만 바꾼다면 isDragging으로 충분합니다.
onSelfDragMove모니터의 onDragMove.
onSelfDragEnd, 드롭이든 취소든모니터의 onDragEnd(드롭)와 onDragCancel. onDragSettle은 오버레이가 안착한 뒤 둘 다 받습니다.
onSelfDragCancel모니터의 onDragCancel.
draggable의 onHover, onLeave, 그리고 그 위를 지나는 드래그에 대한 onDragStart…그 요소에 makeDroppable: onDragEnter, onDragLeave. 리스트의 행에는 아무것도 필요 없습니다.
droppable의 events.onEnter, onLeaveonDragEnter, onDragLeave.
droppable의 events.onDroponBeforeDrop이 결정하고, 그다음 onDrop이 적용합니다. onDrop은 동기이며, 반환값은 무시됩니다.
droppable의 events.onValidateacceptMode: 'some'과 함께 쓰는 onBeforeDrop.
—새로 추가: 프로바이더의 @drag-start … @drag-settle, 그리고 모니터의 onDragOver, onBeforeDrop, onDragResume.

v2의 onDrop은 값이나 값의 Promise를 반환해 드롭을 결정할 수 있었습니다. v3에서는 그것이 리스트, 트리, 드롭 대상, 모니터에 있는 onBeforeDrop의 일입니다.

v2 onDrop의 반환값v3 onBeforeDrop의 반환값
아무것도 없음, 또는 true: 드롭이 진행됨'accept', true, 또는 아무것도 없음.
false: 이 드롭은 거절되고 드래그는 계속됨'continue'. 이제 false는 드롭을 거부합니다.
거부된 Promise나 예외: 드래그가 취소됨'reject' 또는 false. 예외도 거부합니다.
Promise. 그동안 state는 'pending'Promise. 그동안 status는 'dropping'이고, 오버레이는 data-dnd-overlay="dropping" 상태로 기다립니다.
onValidate가 IDragItem[]을 반환id 배열: 그 항목만 드롭되고 나머지는 원래 자리로 돌아갑니다.

먼저 모니터가 답하고, 그다음 대상이 답합니다. 그 뒤 대상의 onDrop이 실행되고, 리스트가 새 배열을 기록하며, onDragEnd가 이어집니다. 자세한 내용은 드롭 결정하기와 이벤트 API에 있습니다.

타입

v2v3
IDragEventDnDEvent(드롭 대상, 모니터), SortableDropEvent(리스트), DnDEndEvent(onDragEnd).
IDragItemDragItem.
IDropZoneContextOver.
IHoveredDraggableContext대응 없음.
ISuggestSortResult, ISuggestSwapResult, ISuggestCopyResult, ISuggestRemoveResultSortableDropEvent: result, to, mode, partner.
IDropValidateEventonBeforeDrop 안의 DnDEvent.
IPlacementcenter가 없는 Placement.
ICoordinatesCoordinates.
IDragActivationOptionsActivationOptions.
IAutoScrollOptionsAutoScrollOptions.
TDnDStateDnDStatus.
IDnDProviderExternalDnDState, useDnD()가 반환하는 것.
IDraggableEvents, IDroppableEventsDraggableOptions, DroppableOptions, DnDMonitor의 콜백.

Nuxt

npm i @vue-dnd-kit/nuxt@^3 @vue-dnd-kit/core@^3
v2 (@vue-dnd-kit/nuxt 1.x)v3 (@vue-dnd-kit/nuxt 3.x)
makeDraggable, makeDroppable, makeSelectionArea, makeConstraintArea, makeAutoScroll, useDnDProvider 자동 임포트makeDraggable, makeDroppable, makeSortableList, makeSortableItem, makeSortableTree, makeSelectionArea, makeDragArea, useDnD, useDnDMonitor 자동 임포트.
타입은 자동 임포트되지 않음주요 타입은 자동 임포트됩니다(DnDEvent, SortableDropEvent, OverlayProps…). 범용 타입(Key, Active, Over)은 직접 임포트합니다.
—모디파이어, 충돌 감지 함수, 센서는 요청할 때만: vueDndKit: { autoImports: { modifiers: true, collision: true, sensors: true } }.
DnDProvider와 DragPreview가 전역같으며, prefix 옵션이 있습니다.
레이아웃의 프로바이더 안에 둔 <DragPreview />지우세요. 오버레이는 프로바이더가 렌더링합니다. DragPreview는 id를 받으며 오버레이 컴포넌트 안에서 씁니다.
peer 의존성 @vue-dnd-kit/core 2peer 의존성 @vue-dnd-kit/core 3, Nuxt 4.

nuxt.config.ts의 modules: ['@vue-dnd-kit/nuxt']는 그대로입니다. 설치를 참고하세요.

전과 후

draggable과 드롭 영역

v2

Card.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';

const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragging } = makeDraggable(el, {
  groups: ['task'],
  dragHandle: '.handle',
  data: () => props.card,
});
</script>

<template>
  <div ref="el" tabindex="0" :class="{ dragging: isDragging }">
    <span class="handle">⋮⋮</span>
    {{ card.title }}
  </div>
</template>
Zone.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';

const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragOver, isAllowed } = makeDroppable(el, {
  groups: ['task'],
  events: {
    onDrop: (e) => emit('drop', e.draggedItems.map((item) => item.data as Card)),
  },
});
</script>

<template>
  <div ref="el" :class="{ over: isDragOver && isAllowed }"><slot /></div>
</template>

v3

Card.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';

const props = defineProps<{ card: Card }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragging } = makeDraggable(el, {
  id: () => props.card.id,   // read back as `active.id`
  type: 'task',              // matched by the targets' `accept`
  handle: '.handle',
  data: () => props.card,
});
</script>

<template>
  <div ref="el" :class="{ dragging: isDragging }">
    <button type="button" class="handle" :aria-label="`Move ${card.title}`">⋮⋮</button>
    {{ card.title }}
  </div>
</template>
Zone.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable } from '@vue-dnd-kit/core';

const emit = defineEmits<{ drop: [cards: Card[]] }>();
const el = useTemplateRef<HTMLElement>('el');

const { isOver, isAccepting } = makeDroppable(el, {
  accept: 'task',
  onDrop: (e) => emit('drop', e.active.items.map((item) => item.data as Card)),
});
</script>

<template>
  <div ref="el" :class="{ over: isOver && isAccepting }"><slot /></div>
</template>
  • 카드는 tabindex, role, aria-* 속성을 스스로 받습니다. <button> 핸들은 포커스를 받을 수 있고, 키보드 드래그는 거기서 시작합니다.
  • groups는 draggable에서는 type, 대상에서는 accept가 되었습니다. 콜백은 events 객체에서 나왔습니다.
  • isAccepting 없이 isOver만 참이면 드래그를 거부하는 대상입니다. 거기에 드롭하면 원래 자리로 돌아갑니다.

정렬 가능한 리스트

v2

TaskList.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type IDragEvent } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';

const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');

makeDroppable(
  el,
  {
    events: {
      onDrop(e: IDragEvent) {
        const r = e.helpers.suggestSort('vertical');
        if (r) tasks.value = r.targetItems as Task[];
      },
    },
  },
  () => tasks.value,
);
</script>

<template>
  <div ref="el">
    <TransitionGroup name="list">
      <TaskItem v-for="(task, index) in tasks" :key="task.id" :task="task" :index="index" :tasks="tasks" />
    </TransitionGroup>
  </div>
</template>
TaskItem.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';

const props = defineProps<{ task: Task; index: number; tasks: Task[] }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragging } = makeDraggable(el, { id: props.task.id }, () => [props.index, props.tasks]);
</script>

<template>
  <div ref="el" :class="{ 'is-dragging': isDragging }">{{ task.title }}</div>
</template>

v3

TaskList.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';

const tasks = defineModel<Task[]>({ required: true });
const el = useTemplateRef<HTMLElement>('el');

const { items } = makeSortableList(el, { items: tasks });
</script>

<template>
  <ul ref="el">
    <TaskItem v-for="task in items" :key="task.id" :task="task" />
  </ul>
</template>
TaskItem.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableItem } from '@vue-dnd-kit/core';

const props = defineProps<{ task: Task }>();
const el = useTemplateRef<HTMLElement>('el');

const { isPlaceholder } = makeSortableItem(el, { item: () => props.task });
</script>

<template>
  <li ref="el" :class="{ slot: isPlaceholder }">{{ task.title }}</li>
</template>
  • 초안인 items를 렌더링하세요. 드래그하는 동안 순서가 바뀝니다. 드롭하면 tasks가 새 순서를 받고, defineModel이 update:modelValue를 내보냅니다.
  • 인덱스도, 페이로드도, onDrop도, <TransitionGroup>도 없습니다. 행은 알아서 미끄러집니다.
  • 드래그 중인 행은 안착할 자리로서 리스트에 남습니다. 숨기지 말고 isPlaceholder나 [data-dnd-placeholder]로 스타일을 지정하세요.

항목을 공유하는 리스트

v2

Board.vue
// Each column's makeDroppable calls this from its onDrop, with its array as the payload.
function applySort(e: IDragEvent) {
  const r = e.helpers.suggestSort('vertical');
  if (!r) return;

  const src = e.draggedItems[0]?.items;
  const tgt = e.hoveredDraggable?.items ?? e.dropZone?.items;

  if (src === todo.value) todo.value = r.sourceItems as Task[];
  else if (src === done.value) done.value = r.sourceItems as Task[];

  if (!r.sameList) {
    if (tgt === todo.value) todo.value = r.targetItems as Task[];
    else if (tgt === done.value) done.value = r.targetItems as Task[];
  }
}

v3

Column.vue
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import TaskItem from './TaskItem.vue';

const props = defineProps<{ column: { id: string; tasks: Task[] } }>();
const el = useTemplateRef<HTMLElement>('el');

const { items } = makeSortableList(el, {
  id: props.column.id,                   // `from.list` and `to.list` in drop events
  items: toRef(props.column, 'tasks'),   // writable: the parent passes a reactive column
  group: 'board',                        // same name: tasks move between these lists
});
</script>

<template>
  <ul ref="el">
    <TaskItem v-for="task in items" :key="task.id" :task="task" />
  </ul>
</template>
Board.vue
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';

const columns = reactive([
  { id: 'todo', tasks: [{ id: 't1', title: 'Write the docs' }] },
  { id: 'done', tasks: [] },
]);
</script>

<template>
  <Column v-for="column in columns" :key="column.id" :column="column" />
</template>

드롭은 두 배열 모두에 기록합니다. 작업이 떠난 배열과 들어간 배열입니다. TaskItem.vue는 위의 정렬 가능한 리스트에 있는 것과 같습니다. 복사본을 나눠 주는 팔레트는 suggestCopy()와 copyOnly 플래그로 만들었지만, 이제는 리스트입니다.

Palette.vue
makeSortableList(el, {
  items: blocks,
  group: { name: 'board', pull: 'clone', put: false },  // copies go out, nothing comes in
  sort: false,                                          // its own order stays
  clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});

Pinia 스토어나 직접 기록하지 않는 다른 상태라면 items를 getter로 넘기고, onDrop에서 e.preventDefault()를 호출한 뒤 e.result를 적용하세요. 제어 리스트를 참고하세요.

커스텀 오버레이

v2

TaskItem.vue
makeDraggable(el, {
  render: markRaw(TaskOverlay),
  data: () => props.task,
});
TaskOverlay.vue
<script setup lang="ts">
import { computed } from 'vue';
import { useDnDProvider } from '@vue-dnd-kit/core';

const { entities } = useDnDProvider();
const task = computed(() => {
  const [node] = entities.draggingMap.keys();
  return node ? (entities.draggableMap.get(node)?.data?.() as Task) : null;
});
</script>

<template>
  <div class="task-preview">{{ task?.title }}</div>
</template>

v3

TaskItem.vue
makeDraggable(el, {
  id: () => props.task.id,
  overlay: TaskOverlay,
  data: () => props.task,
});
TaskOverlay.vue
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';

const props = defineProps<{ items: DragItem[]; over: Over | null; accepted: boolean }>();
const task = computed(() => props.items[0]!.data as Task);
</script>

<template>
  <!-- Fills the overlay box, which takes the size of the slot. -->
  <div class="task-preview" :class="{ refused: over && !accepted }">
    {{ task.title }}
    <span v-if="items.length > 1" class="count">{{ items.length }}</span>
  </div>
</template>

<style scoped>
.task-preview { width: 100%; height: 100%; }
</style>
  • 컴포넌트는 드래그를 props로 받습니다. items(드래그 중인 모든 항목, 집은 항목이 맨 앞), over, accepted, status, mode, to. 읽는 것만 선언하세요. 리스트의 행이라면 리스트 항목은 items[0].item입니다.
  • 그대로 넘기세요. 바꿔 끼울 때는 shallowRef를 쓰고, 깊은 ref는 절대 쓰지 마세요.

프로바이더의 슬롯도 같은 식으로 바뀌며, 컴포넌트가 더 이상 스스로 위치를 정하지 않습니다.

v2

App.vue
<DnDProvider>
  <RouterView />
  <template #preview>
    <MyOverlay />  <!-- moved itself with useDnDProvider().preview.position -->
  </template>
</DnDProvider>

v3

App.vue
<DnDProvider>
  <RouterView />
  <template #overlay="{ items, over, accepted }">
    <MyOverlay :items="items" :over="over" :accepted="accepted" />
  </template>
</DnDProvider>

DragPreview를 <Transition>으로 감싸던 팝인 효과는 @starting-style을 쓴 overlay-class가 됩니다. 들어 올리고, 기울이고, 내려놓기를 참고하세요.

확인을 받는 드롭

v2

Zone.vue
makeDroppable(el, {
  events: {
    onDrop: (e) =>
      new Promise<void | boolean>((resolve) => {
        if (!window.confirm('Drop here?')) return resolve(false); // keep dragging
        place(e.draggedItems);
        resolve();
      }),
  },
});

v3

Zone.vue
makeDroppable(el, {
  onBeforeDrop: () => (window.confirm('Drop here?') ? 'accept' : 'continue'),
  onDrop: (e) => place(e.active.items),
});

리스트도 같은 방식으로 묻고, Promise를 쓰면 직접 만든 다이얼로그를 열 수 있습니다. Promise가 끝날 때까지 오버레이는 놓은 자리에서 기다립니다.

Column.vue
makeSortableList(el, {
  items: toRef(props.column, 'tasks'),
  group: 'board',
  // `ask` opens a dialog and resolves 'accept', 'continue' or 'reject'.
  onBeforeDrop: (e) => (e.sameList ? 'accept' : ask(e)),
});

섞인 선택에서 맞는 부분만 받던 영역(groupMatch: 'some'):

v2

Zone.vue
makeDroppable(el, {
  groups: ['frontend'],
  groupMatch: 'some',
  events: {
    onDrop: (e) => place(e.draggedItems), // only the items of the group arrive
  },
});

v3

Zone.vue
import type { DragItem } from '@vue-dnd-kit/core';

const fits = (item: DragItem) => item.type.length === 0 || item.type.includes('frontend');

makeDroppable(el, {
  accept: 'frontend',
  acceptMode: 'some',
  onBeforeDrop: (e) => e.active.items.filter(fits).map((item) => item.id), // the others fly back
  onDrop: (e) => place(e.active.items.filter(fits)), // `active.items` still lists every dragged item
});

리스트는 코드 없이 이렇게 합니다. acceptMode: 'some'이면 group.put이 받는 항목은 안착하고, 나머지는 원래 자리로 돌아갑니다.

업그레이드할 때 주의할 점

  • 범위를 지정해 v3를 설치하세요. package.json에 ^3. npm update는 ^2를 v2에 그대로 둡니다.
  • 초안을 렌더링하세요. 리스트는 원본 배열이 아니라 makeSortableList가 반환하는 items를 렌더링합니다. 원본 배열을 렌더링하면 드롭할 때까지 아무것도 움직이지 않습니다.
  • 두 번 정렬하지 마세요. 리스트에서 suggestSort 코드를 지우세요. 리스트는 onDrop 뒤에 각 드롭을 쓰기 가능한 items ref에 기록합니다. 직접 적용하려면 e.preventDefault()를 호출하고 e.result를 쓰세요.
  • false는 드롭을 거부합니다. v2에서는 false로 resolve된 onDrop이 드래그를 계속했습니다. v3에서는 onBeforeDrop에서 'continue'를 반환하세요. onDrop에서 반환한 Promise는 무시됩니다.
  • 리스트는 같은 group 안에서만 공유합니다. group이 없는 리스트는 다른 어떤 리스트와도 주고받지 않습니다.
  • 행의 key는 id로 하세요. :key="index"는 엉뚱한 행을 애니메이션합니다. id도 데이터에서 넘기세요. 기본 id는 컴포넌트 인스턴스마다 새로 만들어집니다.
  • <TransitionGroup>도, transform에 대한 transition도 쓰지 마세요. 라이브러리가 행을 직접 움직이므로 움직임이 두 번 애니메이션됩니다.
  • 터치 우회책을 되돌리세요. 항목 전체에 건 touch-action: none은 손가락 아래에서 페이지가 스크롤되지 않게 막습니다. v3는 대신 길게 누르기로 터치 드래그를 시작합니다. 바로 드래그해야 하는 핸들에는 activation: { touch: { distance: 4 } }와 함께 남겨 두세요.
  • 오래된 속성을 지우세요. 라이브러리는 tabindex, role, 필요한 aria-*를 설정하고 직접 지정한 속성은 절대 덮어쓰지 않으므로, 남아 있는 role이나 aria-grabbed는 남겨 둔 그대로 유지됩니다.
  • 오버레이 스타일을 다시 지정하세요. .dnd-kit-preview는 없어졌습니다. [data-dnd-overlay='dragging'] 안에 있는 것에 스타일을 지정하고, 박스의 transform은 절대 건드리지 마세요. 오버레이 컴포넌트의 루트에 width: 100%; height: 100%를 주고, 레이아웃에서 <DragPreview />를 빼세요.
  • 자동 스크롤은 켜져 있습니다. v2에서 스크롤되지 않던 곳에는 :auto-scroll="false"를 넘기세요.
  • 선택 사각형에는 키가 필요 없습니다. makeSelectionArea는 빈 공간을 그냥 누르기만 해도 그립니다. v2의 Ctrl을 유지하려면 modifier를 추가하세요.
  • 프로바이더는 하나, 영역은 그 안에. 항목은 프로바이더를 넘나들 수 없습니다. 규칙을 나누려고 구역마다 프로바이더를 쓰던 페이지에는 프로바이더 하나와 구역마다 makeDragArea가 필요합니다.
  • 인자는 두 개입니다. makeDraggable과 makeDroppable은 ref와 옵션 객체를 받으며, 페이로드는 없습니다.