Front-End/Project

[WizSched] Drag & Drop 보드 컴포넌트 - (1) dnd-kit 적용

Y0ungZ 2024. 7. 27. 07:06
🍀목차
목표
dnd-kit을 선택한 이유
dnd-kit

 

 

목표

Jira의 칸반보드.
Jira의 칸반보드

 

할 일을 관리하는 Drag & Drop 보드 컴포넌트를 만들기 위해 dnd-kit을 사용해 본다.


dnd-kit을 선택한 이유

 처음에는 HTML Drag and Drop API, useRef 훅 등을 사용해서 직접 만들어보려 했지만, 두 개의 보드 사이를 드래그 가능한 요소가 이동해야 함, 마우스-키보드-포인터 이벤트 리스너 설정, 웹 접근성 등... Drag & Drop은 필요한 기능이나 예상시간 이상이 소요될 것이라 판단되어 라이브러리를 사용하였다.

 

가장 유명한 라이브러리는 react-dndreact-beautiful-dnd였고, dnd-kit는 둘에 비해서는 신생 라이브러리였다. 셋의 비교를 해보았다.

 

  • react-beautiful-dnd : 간단한 기능을 가진 dnd를 구현하기에 편해 보임, 셋 중 크기가 제일 큼, Atlassian이 지원(했었음?), 커스텀은 불편, 유지보수가 안 되는 편 ➡️ @hello-pangea/dnd로 후속 버전(커뮤니티 포크)이 만들어져 유지보수되고 있음
  • react-dnd : 셋 중 가장 크기가 작음, 러닝커브 높은 편(다양한 내장 훅, 컨텍스트 이해 필요), 이해한다면 복잡한 dnd 시나리오 구현도 유연한 편.
  • dnd-kit : 스토리북보면 필요한 기능 다 있었음, 둘에 비해서 레퍼런스 적은 편, 러닝커브 있음(react-dnd보다는 낮아 보였음), 크기 큰 편 아님, Accessibility 문서가 가장 잘 되어 있음 ➡️ 전반적으로 공식 문서가 제일 상세하고 읽기 편했음.

 

npm trends로 비교해본 사진 - (1) 다운로드.
npm trends로 비교해본 사진 - (1) 다운로드.
npm trends로 비교해본 사진 - (2) 스탯
npm trends로 비교해본 사진 - (2) 스탯

 

 공식 문서가 굉장히 상세한 점이 선택에 큰 부분을 차지했다. supabase도 헤매고 있다가도 공식 문서에 검색해 보면 다 나와있어 편했는데, dnd-kit 또한 웹 접근성 문서를 잠깐 읽었음에도 dnd 요소 구성 시 어떤 부분을 고려했는지, 고려해야 하는지 이해하기 쉬워 결정하게 되었다.

 

이제 dnd-kit를 찍먹🥄 해보자.

 

 

dnd-kit

두 가지 주요 개념을 사용한다. 결합하여 Drag&Drop 컴포넌트를 만들 수 있다.

  • Draggable elements : useDraggable 훅을 사용해서 DOM 노드를 droppable 컨테이너 위로 끌어다 놓을 수 있게 만듦(드롭 가능한 영역).
  • Droppable areas : useDrappable 훅을 사용해서 DOM 노드를 drappable 영역으로 전환하여 draggable 요소를 놓을 수 있게 만듦(드래그 가능한 요소).
npm install @dnd-kit/core
// react-dom이 설치되어 있지 않다면, 반드시 설치해야함.
npm install react react-dom

 

 

Context provider

useDraggable, useDroppable 훅은 DndContext로 래핑 된 컴포넌트 안에서 올바르게 동작한다. DndContext에 이벤트 리스너를 추가해 발생하는 다양한 이벤트에 응답할 수 있다. 또한 기본적으로 Pointer, Keyboard sensor가 활성화되어 제공된다.

import React from 'react';
import {DndContext} from '@dnd-kit/core';

import {Draggable} from './Draggable';
import {Droppable} from './Droppable';

function App() {
  return (
    <DndContext>
      <Draggable />
      <Droppable />
    </DndContext>
  )
}

// https://docs.dndkit.com/introduction/getting-started#context-provider

 

스크린리더 사용자를 위해 제공되는 DndContext의 기본 announcements를 커스텀 하고 싶다면 아래와 같이 작성한다.

function App() {
  const items = useState(['Apple', 'Orange', 'Strawberries', 'Raspberries']);
  const getPosition = (id) => items.indexOf(id) + 1; // prefer position over index
  const itemCount = items.length;
  
  const announcements = {
    onDragStart({active}) {
      return `Picked up sortable item ${active.id}. Sortable item ${active.id} is in position ${getPosition(id)} of ${itemCount}`;
    },
    onDragOver({active, over}) {
      if (over) {
        return `Sortable item ${active.id} was moved into position ${getPosition(over.id)} of ${itemCount}`;
      }
    },
    onDragEnd({active, over}) {
      if (over) {
        return `Sortable item ${active.id} was dropped at position ${getPosition(over.id)} of ${itemCount}`;
      }
    },
    onDragCancel({active}) {
      return `Dragging was cancelled. Sortable item ${active.id} was dropped.`;
    },
  };
  
  return (
    <DndContext
      accessibility={
        announcements,
      }
    >
    
// https://docs.dndkit.com/guides/accessibility#screen-reader-announcements-using-live-regions

 

대표적으로 많이 사용할 것 같은 이벤트는 아래처럼 보였다.

  • dragstart : 사용자가 드래그 가능 요소를 선택하고 드래그 시작 시 발생. ex. 요소의 어떤 데이터를 전송할 것인지 설정
  • dragend : 사용자가 드래그 가능 요소를 놓을 때 발생한다. ex. 드래그 작업 완료 후, 원래 요소의 스타일 복원 
  • dragover : 드래그된 요소가 드롭 가능한 영역 위에 있을 때 계속해서 발생한다. ex. 드롭 가능한 영역이 드래그된 요소를 수락할지 결정

 

 

Droppable

 useDroppable 훅을 사용해서 만든다. DOM 요소에 대한 ref를 전달하고, 고유 id 애트리뷰트를 제공해야 한다. 

 

import { useDroppable } from '@dnd-kit/core';
import { PropsWithChildren } from 'react';

const Droppable = ({ children }: DrappableProps) => {
  const { isOver, setNodeRef } = useDroppable({
    id: 'droppable',
  });

  const isOverClass = isOver ? 'text-amber-500' : 'text-black';

  return (
    <div ref={setNodeRef} className={`${isOverClass}`}>
      {children}
    </div>
  );
};

interface DrappableProps extends PropsWithChildren {}

export default Droppable;

 

draggable elemenet가 droppable 영역 위로 이동하면 훅의 isOver 속성이 true가 된다. 아래 영상으로 텍스트 컬러가 변하는 것을 확인할 수 있다.

Draggable

 useDraggable 훅을 사용해서 만든다. DOM 요소에 listeners, ref, 고유 id 애트리뷰트를 제공해야 한다.

해당 훅 사용 시 기본적으로 tabindex="0"으로 설정된다. 키보드 접근성에서 대화형 + 키보드 포커스를 받는 사용자 정의 요소에는 명시적으로 tabindex가 0으로 할당되어 있어야 하기 때문이다(draggable은 role 속성을 button으로 제공하고 있다. 그러므로 div로 만들어도 role="button"이 등록되기에 애초에 button 태그로 만드는 것도 좋다).

 

드래그하며 실제로 화면에서 움직이는 것을 보려면  translate, transform속성을 사용해야 한다. 

 

  • translate : 요소를 현재 위치에서 x축과 y축으로 얼마나 이동할 것인지. translate(50px, 100px)은 요소를 오른쪽으로 50px, 아래로 100px 이동시킨다. translate3d의 경우 z축까지 추가해 3차원 벡터 사용.
  • transform : 요소의 위치, 크기, 회전 등을 변경하는 속성. 
import React from 'react';
import {useDraggable} from '@dnd-kit/core';

function Draggable(props) {
  const {attributes, listeners, setNodeRef, transform} = useDraggable({
    id: 'draggable',
  });
  
  // 아래의 스타일을 주지 않으면 드래그 시에 아무런 효과가 나타나지 않음.
  const style = transform ? {
    transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
  } : undefined;

  
  return (
    <button ref={setNodeRef} style={style} {...listeners} {...attributes}>
      {props.children}
    </button>
  );
}

// https://docs.dndkit.com/introduction/getting-started#draggable

 

dnd-kit에서 제시하는 권장사항은 아래와 같다. 

 

  • 성능상의 이유로, 위치 속성은 transform을 권장. transform은 top, left, margin과 같은 요소보다 layout, repaint에 최적화되어 있다.
  • draggable 컴포넌트의 z-index를 변경해서 다른 요소 위에 표시될 수 있게 할 수 있다. 드래그하는 동안 요소가 다른 요소의 위에 표시되도록 높게 설정할 것을 고려하라는 의미 같다.
  • 한 컨테이너에서 다른 컨테이너로 이동해야 하는 경우 사용자 경험 측면에서 <DragOverlay> 컴포넌트를 사용하는 것이 좋다.

 

Drag Overlay

문서

<DragOverlay> 컴포넌트는 사용자 경험을 향상하는 데 큰 도움이 되는 컴포넌트이다. draggable 요소를 드래그 시 사용자에게 명확히 드래그가 진행 중임을 전달할 수 있는 렌더링 방법을 제공한다. 미리보기라고 생각하면 쉽다. 위에서 작성했던 dnd-kit이 제시하는 권장사항에도 한 컨테이너에서 다른 컨테이너로 이동해야 하는 경우 DragOverlay 사용을 강력히 권장하고 있다. 기존 컨테이너에서 마운트를 해제하고 다른 컨테이너에 다시 마운트 할 수 있게 도와준다. 

 

 DragOverlay 컴포넌트 자체는 조건부로 렌더링 할 시 드롭 애니메이션이 작동하지 않는다. 그렇기에 draggable 컴포넌트 외부에 DragOverlay를 렌더링 하는 식으로 분리해야 한다. 

 

아래 영상은 DragOverlay로 드래그 시 test라는 미리 보기를 제공하는 예제이다.

 

 

 

 

Sortable

문서

사용하려면 sortable preset 설치가 필요하다.

npm install @dnd-kit/sortable

 

DndContext가 드래그 앤 드롭 인터랙션 전반을 관리했다면, SortableContext는 리스트나 그리드 형태의 항목들을 드래그 앤 드롭으로 재정렬할 수 있게 해 준다. 항목을 보다 쉽게 이동하고 재배치시킬 수 있어, 사용자 경험을 개선하는 데 유용하다.

import React, {useState} from 'react';
import {DndContext} from '@dnd-kit/core';
import {SortableContext} from '@dnd-kit/sortable';

function App() {
  const [items] = useState(['1', '2', '3']);

  return (
    <DndContext onDragEnd={handleDragEnd}>
      <SortableContext items={items}>
        {/* ... */}
      </SortableContext>
    </DndContext>
  );
  
  function handleDragEnd(event) {
    /* ... */
  }
}

// https://docs.dndkit.com/presets/sortable#sortable-context

 

useSortable 훅은 useDroppable, useDraggable 훅을 결합하여 추상화시킨 훅으로, 개별 요소를 드래그와 드롭 대상으로 연결시킨다. 그리고 SortableContext는 useSortable로 감싸진 항목들을 감싸는 컨텍스트이다. items, strategy를 설정하여 sortable한 항목들과 정렬 전략을 정의한다.

 

import React from 'react';
import {useSortable} from '@dnd-kit/sortable';
import {CSS} from '@dnd-kit/utilities';

function SortableItem(props) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
  } = useSortable({id: props.id});
  
  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  };
  
  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      {/* ... */}
    </div>
  );
}

// https://docs.dndkit.com/presets/sortable#usesortable

 

Sensors

 SortableContext를 사용하며 보다 다양한 사용자 입력 방식을 지원하기 위해 센서를 설정한다. 기본적으로 DndContext에는 포인터, 키보드 센서를 사용하도록 되어있지만 SortableContext의 KeyboardCoordinates를 정확히 전달하기 위해 재설정해주는 것이라고 이해했다. 

 

import {
  DndContext,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
} from '@dnd-kit/core';
import {
  SortableContext,
  sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';

function App() {
  const [items] = useState(['1', '2', '3']);
  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  );

  return (
    <DndContext sensors={sensors}>
      <SortableContext items={items}>
        {/* ... */}
      </SortableContext>
    </DndContext>
  );
}

// https://docs.dndkit.com/presets/sortable#sensors

 

 

 

공식문서 Pushing things a bit further 부분을 활용하여 아래와 같은 예시를 만들었다.

 

 touch-action

개발하며 모바일 환경으로 테스트 하니 draggable 요소들이 매우 불편하게 이동되었다. overflow : hidden을 주니 해당 현상은 사라졌지만 수직-수평 스크롤도 사라졌고, overflow-x : hidden은 여전히 같은 문제가 발생했다.

그러다 draggable 문서를 천천히 읽어보니 touch-action 속성을 발견하게 되었다. dnd-kit에서는 Draggable요소에 touch-action 속성을 지정하는 것을 권장하고 있는데, 해당 속성으로 터치스크린 사용자가 요소에 브라우저에 내장된 영역 조작 방법을 설정하게 된다. 일반적으로 모바일 장치에서 스크롤을 방지하려면 draggable 요소에 touch-action을 none으로 설정하는 것이 좋다. 

 

📌 For Pointer Events, there is no way to prevent the default behaviour of the browser on touch devices when interacting with a draggable element from the pointer event listeners. Using touch-action: none; is the only way to reliably prevent scrolling for pointer events.

Further, using touch-action: none; is currently the only reliable way to prevent scrolling in iOS Safari for both Touch and Pointer events.

 

touch-action 설정하지 않았을 때.

 

 

 

touch-action : none; 설정 후.

 

 

 

 해당 글에서는 dnd-kit를 적용하여 가장 간단한 예시를 구현해 보았다.

다음으로는 본격적으로 task들을 만들고, Sortable Context 등을 활용해서 구현하는 과정을 작성하려고 한다.

 

 

 

참고자료 

https://docs.dndkit.com/

https://docs.dndkit.com/guides/accessibility