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
yarn add @vue-dnd-kit/core@^3
pnpm add @vue-dnd-kit/core@^3
bun add @vue-dnd-kit/core@^3
| 패키지 | v2 | v3 |
|---|---|---|
@vue-dnd-kit/core | 2.4.x | 3.x |
@vue-dnd-kit/nuxt | 1.x | 3.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 문서는 페이지 위쪽의 버전 메뉴에서 계속 볼 수 있습니다.
권장하는 작업 순서:
- 패키지를 업그레이드하고 프로바이더의 props 이름을 바꾸세요(아래).
- 정렬 가능한 리스트를 각각
makeSortableList와makeSortableItem으로 바꾸고, 그 드롭 핸들러를 삭제하세요. - 일반 draggable과 드롭 영역을 옮기세요.
- 커스텀 미리보기를 오버레이 컴포넌트나 프로바이더의
#overlay슬롯으로 옮기세요. makeConstraintArea와makeAutoScroll을makeDragArea로 바꾸거나, 아무것도 쓰지 마세요.- v2를 위한 우회책을 삭제하세요. draggable의
tabindex와aria-grabbed, 항목 전체의touch-action: none, 행을 감싼<TransitionGroup>, 미리보기 컴포넌트를 감싼markRaw.
이전과 이후
표의 대응 없음은 v3에 그런 이름이 없다는 뜻입니다. 대신 무엇을 쓸지는 셀에 적혀 있습니다.
한눈에 보기
| v2 | v3 |
|---|---|
DnDProvider | DnDProvider. 새 props와 #overlay 슬롯이 있습니다. |
makeDraggable | 자유로운 드래그에는 makeDraggable, 리스트의 행에는 makeSortableItem. |
makeDroppable | 일반 드롭 대상에는 makeDroppable, 리스트와 트리에는 makeSortableList 또는 makeSortableTree. |
makeSelectionArea | makeSelectionArea. |
makeConstraintArea | makeDragArea. |
makeAutoScroll | 필요 없음: 자동 스크롤은 자동입니다. makeDragArea나 리스트의 autoScroll로 조정합니다. |
useDnDProvider | 상태는 useDnD, 이벤트는 useDnDMonitor. |
injectionKey | useDnDContext()와 provideDnDContext(app, context). |
DragPreview | DragPreview: 이제 드래그 중인 항목 하나의 클론이며, 직접 만드는 오버레이에 씁니다. |
makeSnappedOverlayPosition, <DragPreview :grid> | snapToGrid 모디파이어. |
e.helpers.suggestSort, suggestSwap, suggestCopy, suggestRemove | 리스트에 기본으로 들어 있습니다. 정렬을 참고하세요. |
프로바이더
| v2 | v3 |
|---|---|
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' 또는 null | useDnD().status: 'idle', 'activating', 'dragging', 'dropping' 또는 'selecting'. 'pending'(비동기 드롭)은 'dropping'이고, null은 'idle'입니다. |
pointer.value.current | useDnD().pointer, 모디파이어를 적용한 값. 모든 이벤트에는 시작점으로부터의 오프셋인 delta도 있습니다. |
entities.draggingMap | useDnD().active: id, ids, items, data, type, element, rect. |
entities.selectedSet(요소) | useDnD().selection(id): has, add, delete, toggle, set, clear, ids. |
hovered.droppable, hovered.draggable | useDnD().over: id, data, element, rect, accepted. |
delay.progress, distanceProgress | useDnD().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
| v2 | v3 |
|---|---|
makeDraggable(ref, options, payload) | makeDraggable(ref, options): 세 번째 인자는 없습니다. 리스트의 행은 makeSortableItem(ref, { item })을 씁니다. |
id, 가상 리스트에서 필요 | id: 데이터에서 getter로 넘기세요. 기본값은 useId()에서 오며 컴포넌트 인스턴스마다 새로 만들어집니다. |
data: () => card | data: 값, ref, 또는 getter. active.data와 active.items[i].data로 다시 읽습니다. |
groups: ['dev'] | type: 'dev', 대상의 accept와 비교됩니다. 그룹을 참고하세요. |
dragHandle: '.handle' | handle: 선택자나 요소 ref. |
disabled | disabled. |
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, onSelfDragCancel | draggable에는 대응 없음: 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
| v2 | v3 |
|---|---|
makeDroppable(ref, options, () => items) | makeDroppable(ref, options). 리스트를 담은 영역은 makeSortableList입니다. |
groups | accept: '*', 타입 하나, 타입 목록, 또는 함수. |
groupMatch: 'every', 'some' | acceptMode: 'all', 'some'. |
events.onEnter, events.onLeave | onDragEnter, onDragLeave: 각각 독립된 옵션이며 events 객체는 없습니다. |
false나 Promise를 반환할 수 있던 events.onDrop | onBeforeDrop이 결정하고 onDrop이 적용합니다. 드롭 결정을 참고하세요. |
events.onValidate | 남길 id를 반환하는 onBeforeDrop. |
data | data, over.data로 다시 읽습니다. |
disabled | disabled. |
isAllowed 반환 | isAccepting. |
isDragOver 반환: { top, right, bottom, left, center } | isOver(불리언)와 placement: { top, right, bottom, left }, 포인터가 있는 쪽 절반. center는 없습니다. |
| — | 새로 추가: id, label, priority, overOverlay, overDropAnimation, external(컴퓨터의 파일). |
드롭 이벤트
v2 IDragEvent | v3 |
|---|---|
e.draggedItems | e.active.items: 각각 { id, item, data, label, type }이며, 집은 항목이 맨 앞입니다. 리스트는 e.items와 e.keys도 받습니다. |
e.draggedItems[0].data | e.active.data. |
e.draggedItems[i].index, .items | 리스트: e.from, { list, index }. |
e.dropZone: items, placement, data | e.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.sameList | e.sameList. |
e.helpers.insertAt, removeAt, removeIndexes, swapAt, getInsertSide, isAtZoneStart, isAtZoneEnd | 대응 없음. 리스트에는 필요 없고, 일반 드롭 대상에서는 데이터를 원하는 대로 바꾸세요. |
e.provider | useDnD(). |
| — | 새로 추가: e.pointer, e.delta, e.position(보드에 드롭한 항목을 놓을 위치), e.mode. |
정렬
| v2 | v3 |
|---|---|
() => 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 하나와 applyToTree | makeSortableTree: 컨테이너 하나, 행은 깊이와 함께 평평하게 렌더링됩니다. 트리를 참고하세요. |
v-model을 쓰는 중첩 그룹(그룹마다 droppable 하나) | 리스트 안의 리스트. 모두 같은 group에 두고, 각각 items: defineModel(). |
가상 리스트: 모든 draggable에 id | 초안을 가상화 도구에 넘기고, 행에 id: () => props.item.id. 가상 리스트를 참고하세요. |
전후 코드는 아래에 있습니다. 리스트에 관한 모든 것은 정렬 가능한 리스트와 항목을 공유하는 리스트에 있습니다.
그룹
v2에는 모든 것에 쓰는 groups 옵션 하나가 있었습니다. v3는 이를 둘로 나눕니다.
| v2 | v3 |
|---|---|
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'. |
리스트 영역과 그 행의 groups | makeSortableList의 group: 'board'. group: { name, pull, put }은 무엇이 나가고 들어올 수 있는지 정합니다. |
makeSelectionArea의 groups | accept. |
group이 없는 리스트는 그 자체로 하나의 그룹이며 다른 어떤 리스트와도 항목을 주고받지 않습니다. v2에서는
groups가 없는 영역이 모든 것을 받았습니다.
대상이 받는 드래그를 참고하세요.
오버레이
| v2 | v3 |
|---|---|
프로바이더 안에 렌더링되고 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>, makeSnappedOverlayPosition | snapToGrid 모디파이어. 그림만이 아니라 대상을 찾는 위치와 드롭의 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). |
| 드롭하면 미리보기가 사라짐 | 자기 자리에 안착하거나, 취소하면 원래 자리로 돌아갑니다. |
자세한 내용은 오버레이 가이드에 있습니다.
선택과 멀티 드래그
| v2 | v3 |
|---|---|
makeDraggable이 반환하는 selected | makeDraggable과 makeSortableItem이 반환하는 selected. |
entities.selectedSet, 요소의 집합 | useDnD().selection, id가 key. |
makeSelectionArea: 기본으로 Ctrl(ControlLeft)을 누른 채 | 기본으로 키가 필요 없습니다. 영역의 빈 공간을 누르면 사각형을 그립니다. modifier: { keys: ['Control'], method: 'every' }로 Ctrl을 되살릴 수 있습니다. |
KeyboardEvent.code로 쓰는 modifier.keys | KeyboardEvent.key('Control', 'Shift') 또는 .code('ControlLeft'). |
strategy: 'toggle', 'select' | 같습니다. |
groups | accept. |
events.onSelected(elements) | onSelected(ids). |
disabled | 같습니다. |
선택을 한꺼번에 옮기는 suggestSort() | 리스트가 옮깁니다. multiple: 'group'(기본값), 'gaps' 또는 'stack'이 다른 항목이 도중에 어떻게 할지 정합니다. |
섞인 선택을 위한 onValidate | acceptMode: 'some'과 id를 반환하는 onBeforeDrop. 일부만 받기를 참고하세요. |
자세한 내용은 멀티 드래그와 선택에 있습니다.
축, 경계, 스냅
| v2 | v3 |
|---|---|
makeConstraintArea(el, { axis: 'x' }) | makeDragArea(el, { axis: 'x' }), 그 안에서 시작하는 드래그에 적용됩니다. axis는 draggable, 리스트, 트리, 항목에도 쓸 수 있습니다. |
axis: 'both' | axis를 생략합니다. |
restrictToArea: true | makeDragArea(el, { restrictToArea: true }), 또는 모디파이어 restrictToParent와 restrictToElement(el). |
| 규칙을 나누기 위한 구역별 프로바이더 | 프로바이더 하나와 구역마다 makeDragArea. 그 항목은 여전히 어디로든 움직일 수 있습니다. |
<DragPreview :grid="20">, makeSnappedOverlayPosition | snapToGrid(20) 또는 snapToGrid({ x, y, origin, smooth })를 프로바이더, 영역, 리스트, draggable의 modifiers에. |
| — | 새로 추가: restrictToWindowEdges, restrictToFirstScrollableAncestor, snapCenterToCursor, 그리고 직접 만든 모디파이어. |
자세한 내용은 축, 경계, 그리드에 있습니다.
자동 스크롤
| v2 | v3 |
|---|---|
<DnDProvider :autoScrollViewport="true">, 기본으로 꺼짐 | 기본으로 켜짐, 창과 모든 스크롤 컨테이너에 적용됩니다. :auto-scroll="false"로 끕니다. |
makeAutoScroll(el, { threshold, speed }) | 필요 없음: overflow: auto나 scroll인 요소는 모두 스크롤됩니다. 하나를 조정하려면 makeDragArea(el, { autoScroll: { … } }) 또는 리스트의 autoScroll. |
px 단위, 또는 변마다 정하는 threshold | threshold: 컨테이너 크기에 대한 비율(0.2), 24~96 px 사이. 변마다 정하는 임계값은 대응 없음. |
배수인 speed | maxSpeed(프레임당 25 px)와 acceleration(10). |
disabled: true | autoScroll: false. |
isScrolling | makeDragArea나 useDnD()의 isAutoScrolling. |
키보드와 접근성
| v2 | v3 |
|---|---|
모든 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가 문구를 바꿉니다. |
자세한 내용은 접근성에 있습니다.
이벤트와 드롭 결정
| v2 | v3 |
|---|---|
draggable의 events.onSelfDragStart | useDnDMonitor({ onDragStart })에서 active.ids를 확인합니다. 클래스만 바꾼다면 isDragging으로 충분합니다. |
onSelfDragMove | 모니터의 onDragMove. |
onSelfDragEnd, 드롭이든 취소든 | 모니터의 onDragEnd(드롭)와 onDragCancel. onDragSettle은 오버레이가 안착한 뒤 둘 다 받습니다. |
onSelfDragCancel | 모니터의 onDragCancel. |
draggable의 onHover, onLeave, 그리고 그 위를 지나는 드래그에 대한 onDragStart… | 그 요소에 makeDroppable: onDragEnter, onDragLeave. 리스트의 행에는 아무것도 필요 없습니다. |
droppable의 events.onEnter, onLeave | onDragEnter, onDragLeave. |
droppable의 events.onDrop | onBeforeDrop이 결정하고, 그다음 onDrop이 적용합니다. onDrop은 동기이며, 반환값은 무시됩니다. |
droppable의 events.onValidate | acceptMode: '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에 있습니다.
타입
| v2 | v3 |
|---|---|
IDragEvent | DnDEvent(드롭 대상, 모니터), SortableDropEvent(리스트), DnDEndEvent(onDragEnd). |
IDragItem | DragItem. |
IDropZoneContext | Over. |
IHoveredDraggableContext | 대응 없음. |
ISuggestSortResult, ISuggestSwapResult, ISuggestCopyResult, ISuggestRemoveResult | SortableDropEvent: result, to, mode, partner. |
IDropValidateEvent | onBeforeDrop 안의 DnDEvent. |
IPlacement | center가 없는 Placement. |
ICoordinates | Coordinates. |
IDragActivationOptions | ActivationOptions. |
IAutoScrollOptions | AutoScrollOptions. |
TDnDState | DnDStatus. |
IDnDProviderExternal | DnDState, useDnD()가 반환하는 것. |
IDraggableEvents, IDroppableEvents | DraggableOptions, 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 2 | peer 의존성 @vue-dnd-kit/core 3, Nuxt 4. |
nuxt.config.ts의 modules: ['@vue-dnd-kit/nuxt']는 그대로입니다.
설치를 참고하세요.
전과 후
draggable과 드롭 영역
v2
<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>
<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
<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>
<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
<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>
<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
<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>
<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
// 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
<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>
<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 플래그로
만들었지만, 이제는 리스트입니다.
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
makeDraggable(el, {
render: markRaw(TaskOverlay),
data: () => props.task,
});
<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
makeDraggable(el, {
id: () => props.task.id,
overlay: TaskOverlay,
data: () => props.task,
});
<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
<DnDProvider>
<RouterView />
<template #preview>
<MyOverlay /> <!-- moved itself with useDnDProvider().preview.position -->
</template>
</DnDProvider>
v3
<DnDProvider>
<RouterView />
<template #overlay="{ items, over, accepted }">
<MyOverlay :items="items" :over="over" :accepted="accepted" />
</template>
</DnDProvider>
DragPreview를 <Transition>으로 감싸던 팝인 효과는 @starting-style을 쓴 overlay-class가
됩니다. 들어 올리고, 기울이고, 내려놓기를
참고하세요.
확인을 받는 드롭
v2
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
makeDroppable(el, {
onBeforeDrop: () => (window.confirm('Drop here?') ? 'accept' : 'continue'),
onDrop: (e) => place(e.active.items),
});
리스트도 같은 방식으로 묻고, Promise를 쓰면 직접 만든 다이얼로그를 열 수 있습니다. Promise가 끝날 때까지 오버레이는 놓은 자리에서 기다립니다.
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
makeDroppable(el, {
groups: ['frontend'],
groupMatch: 'some',
events: {
onDrop: (e) => place(e.draggedItems), // only the items of the group arrive
},
});
v3
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뒤에 각 드롭을 쓰기 가능한itemsref에 기록합니다. 직접 적용하려면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와 옵션 객체를 받으며, 페이로드는 없습니다.