관리 메뉴

빵 입니다.

Drag and Drop 해보자(w/ 라이브러리) 본문

프론트엔드/Vue

Drag and Drop 해보자(w/ 라이브러리)

bread-gee 2025. 3. 24. 15:46
반응형

D&D 기능을 구현했습니다.

직접 만들어볼까~ 하다가 구현해야 할 기능 공수에 비해 시간적 여유가 부족해서 라이브러리를 사용했는데요.

3가지 꽤괜 라이브러리를 찾았습니다.

 

📌 SortableJS VS VueDraggable VS Shopify Draggable

SortableJS와 Shopify Draggable는 Vanilla JS 기반이고, VueDraggable는 Vue/Nuxt에 최적화되어 있습니다.

 

특징을 간략하게 소개하자면...

 

◾ SortableJS

animation, ghostClass 등 다양한 옵션을 제공하고, VueDraggable보다 커스텀의 범위가 더 자유롭습니다.(당연, Vue와 상관없이 작동함.)

다만, 배열을 자동으로 업데이트 해주지 않기 때문에 onEnd 이벤트 발생 시 splice()로 배열 직접 업데이트해야 합니다.

 

VueDraggable

VueDraggable는  SortableJS 기반입니다.

SortableJS과 마찬가지로 animation, ghostClass 등 다양한 옵션을 제공하고, 커스텀이 가능합니다.
SortableJS와 가장 큰 다른 점은 VueDraggable은 v-model을 지원하기 때문에 별도로 splice()를 사용하지 않아도 배열이 자동으로 업데이트됩니다.
SortableJS 기반이기 때문에 옵션에 대한 가이드도 SortableJS를 확인하시면 됩니다.

https://github.com/SortableJS/Sortable#event-object-demo

  Shopify Draggable는

Shopify Draggable는  Draggable 기능을 찾다가 알게 된 라이브러리인데요.
드래그한 아이템 복제 및 스타일링 가능하지만, animation이 기본 지원되지 않기 때문에 CSS로 직접 구현해야 합니다.
위 두 라이브러리와 가장 큰 다른 점은 Draggable 되는 건 똑같으나 Swap 방식으로 자리가 변경됩니다.
순서 변경이 아니라 자리 교체가 됩니다.

 

 

그럼 여기서 잠깐.

Draggable과 Swap의 차이점은?

 

📌 Draggable 기능 VS Swap 기능

Draggable은 순서를 변경한다고 생각하면 됩니다.
아이템과 아이템 사이에 다른 아이템를 끌어다 넣을 수 있습니다.

* 빨간색은 내가 잡고 있는 것(grab)

* 초록 화살표는 내가 이동하고 싶은 위치

Draggable ing...
Draggable 완료



Swap은 아이템 교체입니다.
아이템을 끌어다 교체할 아이템에 올리면 두 아이템의 위치가 교체됩니다.

* 빨간색은 내가 잡고 있는 것(grab)

* 초록색은 이동하고 싶은 위치

Swap Before

 

Swap After

 

📌 이슈 사항

각 아이템엔 여러 차트 데이터(ChartJS)가 들어가 있는데, 아이템을 드래그할 때 차트가 노출되지 않는 이슈를 발견했습니다.
다행히 해결 방법을 찾았습니다.
ChartJS는 <canvas />에 차트를 그리는데, 드래그가 시작될 때 해당 <canvas />의 내용을 가져와서 복제된 드래그 아이템의 canvas에 다시 그리면 됩니다.

그러나 산 넘어 산이라고...
차트는 잘 그려지는데, 아이템 내에서 사용하는 UI 프레임워크 컴포넌트 내의 내용이 노출되지 않는 이슈도 있었습니다.
디자인된 scroll 영역 사용을 위해 Quasar 프레임워크의 컴포넌트를 사용하고 있는데, 아이템을 드래그할 때 해당 컴포넌트 내의 내용이 노출되지 않았습니다.

차트는 canvas로 처리하고 프레임워크는 다르게 분기해서 처리할까 하다가 한번에 처리할 수 있는 방법을 생각해 보았습니다.

그리고 해결 방법을 찾았습니다.
아이템 내용을 이미지화 해서 보여주는 방법인데요.
SortableJS에서 제공하는 onClone 메서드를 이용하면 드래그를 시작한 뒤 원본 아이템 요소에 접근이 가능합니다.

htmlToImage 라이브러리를 이용해 원본 아이템(DOM)을 이미지화하고, 해당 이미지를 복제된 드래그 아이템에 append 합니다.

 

📌 구현

저는 Vue3 기반의 프로젝트를 운영하기 때문에 VueDraggable을 설치했습니다.

 

🧿 요구사항

  아이템간 순서 변경(Swap X)

👉🏻 VueDraggable 사용으로 충족

 

  드래그 중인 아이템의 잔상으로 보여지지 않고, 온전한 아이템 형태로 보여져야 한다.

👉🏻 dragOptions의 forceFallback: true, fallbackOnBody: true 설정

드래그 시 잔상만 남는 이슈

 

 드래그 시 자연스럽게 순서 교체가 되어야 한다.(animation 효과)

👉🏻 기본 제공

 

 

🧿 드래그 옵션

const dragOptions = {
    draggable: 'drag-item', // 드래그 대상 item의 클래스 명시
    animation: 200, // 정렬 시 항목을 이동하는 애니메이션 속도
    ghostClass: 'origin-item', // 드롭될 영역 아이템의 클래스
    dragClass: 'dragging-item', // 드래그 중인 아이템의 클래스
    forceFallback: true,
        // true를 설정하면 HTML5에서 제공하는 기본 Drag and Drop 기능을 사용하지 않고,
        // JS 기반의 드래그 시스템을 강제로 사용하게 됩니다.
    fallbackOnBody: true
        // 복제된 DOM 요소를 문서 본문에 추가합니다.
        // 👉🏻 드래그 중인 아이템을 <body> 태그 안에 복제하여 Append 합니다.
}
더보기

❗ 참고
forceFallback를 false로 설정하면 HTML5에서 제공하는 기본 Drag and Drop 기능을 사용하기 때문에,
fallbackOnBody: true로 설정해도 드래그 중인 아이템을 복제(온전한 형태)하지 않습니다.


* HTML5에서 제공하는 기본 Drag and Drop 기능은 드래그 중인 아이템의 온전한 형태를 보여주지 않고, 상하에 Opacity 효과를 줍니다.

 

🧿 HTML

<draggable
    v-model="items"
    v-bind="dragOptions"
    tag="div"
    class="drag-wrap"
    item-key="name"
    @clone="onClone"
>
    <template #item="{ element }">
        <div class="drag-item">{{ element }}</div>
    </template>
</draggable>

 

 

🧿 드래그 이벤트

const onClone = (event: SortableEvent) => {
    setTimeout(() => {
        const target = event.item
        const draggingItem = document.querySelector('.dragging-item')
        if (draggingItem) {
            draggingItem.innerHTML = ''
            const img = new Image()

        htmlToImage
            .toPng(target)
            .then((dataUrl) => {
                img.src = dataUrl
                draggingItem.appendChild(img)
            })
            .catch((error) => {
                console.error(error)
                })
            }
    }, 0)
}

 

 

📌 전체 코드

<script setup lang="ts">
import draggable from 'vuedraggable'
import type { SortableEvent } from 'sortablejs'
import * as htmlToImage from 'html-to-image'

const items = ref<{ id: number; name: string; activation: boolean }[]>([])

const dragOptions = {
    draggable: 'drag-item',
    animation: 200,
    ghostClass: 'origin-item',
    dragClass: 'dragging-item',
    forceFallback: true,
    fallbackOnBody: true
}

const onClone = (event: SortableEvent) => {
    setTimeout(() => {
        const target = event.item
        const draggingItem = document.querySelector('.dragging-item')
        if (draggingItem) {
            draggingItem.innerHTML = ''
            const img = new Image()

        htmlToImage
            .toPng(target)
            .then((dataUrl) => {
                img.src = dataUrl
                draggingItem.appendChild(img)
            })
            .catch((error) => {
                console.error(error)
                })
            }
    }, 0)
}

const onDragEnd = () => {
	return items.value
}
</script>

<template>
    <draggable
        v-model="items"
        v-bind="dragOptions"
        tag="div"
        class="drag-wrap"
        item-key="name"
        @clone="onClone"
    >
        <template #item="{ element }">
        	<div class="drag-item">{{ element }}</div>
        </template>
    </draggable>
</template>

 

 


👀 참고 사이트

SortableJS
https://sortablejs.github.io/Sortable/

VueDraggable
https://sortablejs.github.io/Vue.Draggable/

Shopify Draggable
https://shopify.github.io/draggable/

차트 canvas 복붙
https://codesandbox.io/p/sandbox/vue-template-forked-q3lzcs?file=%2Fsrc%2Fcomponents%2FDraggableGrid.vue%3A86%2C5

 

반응형