๋ฐ˜์‘ํ˜•

์นธ๋ฐ˜ ๋ณด๋“œ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํ™”๋ฉด

 

์ผ๋ฐ˜์ ์œผ๋กœ ์นธ๋ฐ˜ ๋ณด๋“œ๋Š” ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์„ ํ†ตํ•ด ์ปฌ๋Ÿผ์ด๋‚˜ ํƒœ์Šคํฌ ์นด๋“œ์˜ ์ˆœ์„œ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ƒํ˜ธ์ž‘์šฉ์€ dnd-kit ์ด๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” Sortable ํ”„๋ฆฌ์…‹์„ ์ด์šฉํ•˜๋ฉด ๋” ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. dnd-kit์€ ์ฝ”์–ด ํฌ๊ธฐ๊ฐ€ 10kb ์ •๋„๋กœ ๊ฐ€๋ณ๊ณ  ์™ธ๋ถ€ ์˜์กด์„ฑ์ด ์—†๋Š” ์žฅ์ ์ด ์žˆ๋‹ค. ๋“œ๋ž˜๊ทธ ์ œํ•œ, ์• ๋‹ˆ๋ฉ”์ด์…˜, ์ถฉ๋Œ ๊ฐ์ง€ ๋“ฑ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

dnd-kit์„ ์ด์šฉํ•ด ์นธ๋ฐ˜ ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋น„๊ต์  ๊นŒ๋‹ค๋กญ๋‹ค๊ณ  ๋Š๊ผˆ๋˜ ๋ถ€๋ถ„๋“ค์„ ์ •๋ฆฌํ•ด ๋ดค๋‹ค. ์ฝ”๋“œ๋Š” ์•„๋ž˜ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

์นธ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ


ID ๊ธฐ๋ฐ˜ ์ฐธ์กฐ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—”ํ‹ฐํ‹ฐ ๊ฐ„ ๊ด€๊ณ„๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋น ๋ฅธ ์กฐํšŒ ์„ฑ๋Šฅ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฐ ์—”ํ‹ฐํ‹ฐ๋Š” ๊ณ ์œ ํ•œ id๋ฅผ ๊ฐ€์ง€๋ฉฐ, ์™ธ๋ž˜ ํ‚ค(boardId, columnId)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ์„ ์œ ์ง€ํ•œ๋‹ค. ์นธ๋ฐ˜ ๋ณด๋“œ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ปฌ๋Ÿผ์„ ํฌํ•จํ•˜๊ณ , ๊ฐ ์ปฌ๋Ÿผ์€ ๋‹ค์‹œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํƒœ์Šคํฌ๋กœ ๊ตฌ์„ฑ๋˜๋Š” ๊ณ„์ธต์ ์ธ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„๋‹ค.

 

  • Record<TaskId, TaskDef> ํ˜•ํƒœ๋กœ ์ €์žฅํ•˜๋ฉด O(1) ์‹œ๊ฐ„ ๋ณต์žก๋„๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ
  • boardId, columnId ๊ฐ™์€ FK(Foreign Key, ์™ธ๋ž˜ ํ‚ค)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ
  • ํ•„์š”์— ๋”ฐ๋ผ assigneeId, priority ๊ฐ™์€ ํ•„๋“œ๋ฅผ ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Œ

 

 

๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ


๋ฆฌ์ŠคํŠธ๋ฅผ SortableContext๋กœ ๊ฐ์‹ธ๊ณ , ๋ฆฌ์ŠคํŠธ์˜ ๊ฐ ์•„์ดํ…œ์— useSortable ํ›… ๋ฐ˜ํ™˜๊ฐ’์„ ์ ์šฉํ•˜๋ฉด ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์•„์ดํ…œ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฐธ๊ณ ๋กœ useSortable ํ›…์€ useDraggable, useDroppable ํ›…์„ ๊ฒฐํ•ฉํ•˜์—ฌ ๋งŒ๋“  ํ”„๋ฆฌ์…‹์ด๋‹ค.

 

  • SortableContext: ๋ฆฌ์ŠคํŠธ ์ˆœ์„œ ๊ด€๋ฆฌ
  • useSortable: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๊ธฐ๋Šฅ ๋ถ€์—ฌ, ์•„์ดํ…œ ์œ„์น˜ ๋ณ€ํ™”์— ํ•„์š”ํ•œ ์ƒํƒœ์™€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ œ๊ณต.

 

Task๋Š” ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ(์†ํ•ด์žˆ๋Š” ์ปฌ๋Ÿผ)๋ฅผ ๋ฒ—์–ด๋‚˜ ๋‹ค๋ฅธ ์ปฌ๋Ÿผ์œผ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋ฏ€๋กœ, ๋“œ๋ž˜๊ทธ ์•„์ดํ…œ์„ DragOverlay๋กœ ๊ฐ์‹ธ์•ผํ•œ๋‹ค. DragOverlay๋Š” ๊ธฐ์กด ๋ฌธ์„œ ํ๋ฆ„์—์„œ ๋ถ„๋ฆฌ๋˜์–ด ๋ทฐํฌํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์˜ค๋ฒ„๋ ˆ์ด๋ฅผ ๋ Œ๋”๋ง ํ•œ๋‹ค.

// board.tsx
// ...
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';

const Board = () => {
  // ...
  // onDragStart, onDragEnd ๋“ฑ ํ•ธ๋“ค๋Ÿฌ ๋กœ์ง์„ ์ •์˜ํ•œ ์ปค์Šคํ…€ ํ›…
  const { handlers, dragColumnId, dragTaskId, /* ... */ } = useKanbanDnd();

  return (
    <div className="...">
      {/* ๋ฆฌ์ŠคํŠธ๋ฅผ DndContext, SortableContext๋กœ ๊ฐ์‹ธ์ค€๋‹ค */}
      <DndContext {...handlers} id={...} sensors={...} modifiers={...}>
        <SortableContext items={board.columnIds} id={board.id}>
          {board.columnIds.map((columnId) => (
            <Column key={columnId} columnId={columnId} />
          ))}
        </SortableContext>
        <DragOverlay>
          {/* ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์•„์ดํ…œ */}
          {dragColumnId && <Column columnId={toColumnId(dragColumnId)} />}
          {dragTaskId && <Task taskId={toTaskId(dragTaskId)} />}
        </DragOverlay>
      </DndContext>
    </div>
  );
};

 

๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ ํ˜„์žฌ ์š”์†Œ๊ฐ€ ์ปฌ๋Ÿผ์ธ์ง€ ํƒœ์Šคํฌ์ธ์ง€ ํŒ๋ณ„ํ•œ ํ›„, DragOverlay์—์„œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋งํ•ด์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด onDragStart ํ•ธ๋“ค๋Ÿฌ์—์„œ ๊ฐ ํƒ€์ž…(task, column)์— ํ•ด๋‹นํ•˜๋Š” id๋ฅผ ๋ณ„๋„ ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.

// use-drag-state.ts
const [dragColumnId, setDragColumnId] = useState<T>();
const [dragTaskId, setDragTaskId] = useState<T>();

const setDragState = useCallback((type: 'task' | 'column', value: T) => {
  const setStateMap = { task: setDragTaskId, column: setDragColumnId };
  setStateMap[type](value);
}, []);

// use-kanban-dnd.ts
const onDragStart = ({ active }: DragStartEvent) => {
  const dragType = getDragTypes(active).isActiveTask ? 'task' : 'column';
  setDragState(dragType, active.id);
};

 

๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ์—์„  useSortable ํ›…์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’๋“ค์„ ๋“œ๋ž˜๊ทธ ๋Œ€์ƒ ์š”์†Œ์— ์ ์šฉํ•œ๋‹ค. useSortable ํ›…์ด ๋ฐ›๋Š” id, data ๋“ฑ ์ธ์ž๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋กœ ์ „๋‹ฌ๋œ๋‹ค(active, over ์†์„ฑ์— ์ถ”๊ฐ€๋จ).

 

์ „์ฒด ์š”์†Œ๊ฐ€ ์•„๋‹Œ ํŠน์ • ๋ถ€๋ถ„์„ ํด๋ฆญํ–ˆ์„ ๋•Œ๋งŒ ๋“œ๋ž˜๊ทธ๋˜๋„๋ก ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด listeners ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋‹ค๋ฅธ ์š”์†Œ์— ํ• ๋‹นํ•˜๋ฉด ๋œ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ๋“œ๋กญ ์ง€์ ์— ๋“œ๋ž˜๊ทธ ๋Œ€์ƒ ์š”์†Œ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋ง ๋œ๋‹ค. ํ…Œ๋‘๋ฆฌ๋งŒ ๋ณด์ด๊ฑฐ๋‚˜ ํŠน์ • ์Šคํƒ€์ผ์„ ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด isDragging ์ƒํƒœ๋ฅผ ํ™œ์šฉํ•ด์„œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋œ๋‹ค.

// column.tsx
// ...
import { CSS } from '@dnd-kit/utilities';
import { useSortable } from '@dnd-kit/sortable';

const Column = ({ boardId, children }) => {
  const {
    attributes, // ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•œ ARIA ์†์„ฑ (๋“œ๋ž˜๊ทธ ์š”์†Œ์— ์ ์šฉ)
    listeners, // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘์„ ๊ฐ์ง€ํ•˜๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
    setNodeRef, // ์š”์†Œ๋ฅผ ๋“œ๋ž˜๊ทธ ๋Œ€์ƒ์œผ๋กœ ์„ค์ •ํ•˜๋Š” ref ํ•จ์ˆ˜
    transform, // ๋“œ๋ž˜๊ทธ ์š”์†Œ์˜ ์œ„์น˜ ๋ณ€ํ™”๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ (x, y, scaleX, scaleY)
    transition, // ๋“œ๋ž˜๊ทธ ์š”์†Œ์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฐ’
    isDragging, // ํ˜„์žฌ ์š”์†Œ๊ฐ€ ๋“œ๋ž˜๊ทธ ์ค‘์ธ์ง€ ์—ฌ๋ถ€ boolean
  } = useSortable({
    id: boardId, // ์•„์ดํ…œ์˜ ๊ณ ์œ  ID
    data: { type: 'column' } // ์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ
  });

  const style: CSSProperties = {
    transform: CSS.Transform.toString(transform), // ์œ„์น˜ ์ด๋™ ์Šคํƒ€์ผ ์ ์šฉ
    transition, // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ ์ ์šฉ
  };

  if (isDragging) {
      // ๋“œ๋ž˜๊ทธ ์ค‘์ผ ๋•Œ ๋“œ๋กญ ์˜์—ญ์— ํ‘œ์‹œํ•  Placeholder
    return <DropPlaceholder variant={type} style={style} ref={setNodeRef} />;
  }

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

 

๐Ÿ’ก ๋‚ด๋ถ€์—์„œ useSortable ํ›…์„ ํ˜ธ์ถœํ•˜๋Š” Draggable ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์ง„๋‹ค.

 

 

์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ


์ปฌ๋Ÿผ์˜ ์ปจํ…Œ์ด๋„ˆ๋Š” ํ•ญ์ƒ Board์ด๋ฏ€๋กœ ์•„์ดํ…œ์„ ๋“œ๋กญํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ๋˜๋Š” onDragEnd ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ์˜ ์ธ์ž๋กœ ์ „๋‹ฌ๋˜๋Š” active๋Š” ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์•„์ดํ…œ์„, over๋Š” ๋“œ๋กญ ์œ„์น˜์˜ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ฐธ์กฐํ•œ๋‹ค.

// use-kanban-dnd.ts
const onDragEnd = ({ active, over }: DragEndEvent) => {
  // ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์•„์ดํ…œ์ด DragOverlay์—์„œ ๋ Œ๋”๋ง ๋˜์ง€ ์•Š๋„๋ก dragColumnId, dragTaskId ์ดˆ๊ธฐํ™”
  // ํ•˜๋‹จ์— early return ์žˆ์œผ๋ฏ€๋กœ ์ตœ์ƒ๋‹จ์—์„œ ์ดˆ๊ธฐํ™”
  resetDragState();

  if (!over) return; // ๋“œ๋กญ ์˜์—ญ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ
  if (active.id === over?.id) return; // ๊ฐ™์€ ์œ„์น˜๋Š” ์Šคํ‚ต
  if (!getDragTypes(active).isActiveColumn) return; // Column ๋“œ๋ž˜๊ทธ๊ฐ€ ์•„๋‹ˆ๋ฉด ์Šคํ‚ต

  const activeSort = active.data.current?.sortable as ColumnSortable;
  const overSort = over?.data.current?.sortable as ColumnSortable;

    // arrayMove๋Š” dnd-kit ์ž์ฒด์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜
  const newColumnIds = arrayMove(activeSort.items, activeSort.index, overSort.index);
  moveColumn(activeSort.containerId, newColumnIds);
};

 

Sortable ํ”„๋ฆฌ์…‹์„ ์‚ฌ์šฉํ•˜๋ฉด active, over ๊ฐ์ฒด์— sortable์ด๋ผ๋Š” ์œ ์šฉํ•œ ์†์„ฑ์ด ์ถ”๊ฐ€๋œ๋‹ค. ์ด ์†์„ฑ์„ ํ™œ์šฉํ•˜๋ฉด ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์•„์ดํ…œ์ด ์†ํ•œ ์ปจํ…Œ์ด๋„ˆ ID, ์ธ๋ฑ์Šค, ์ปจํ…Œ์ด๋„ˆ ๋‚ด์˜ ์ „์ฒด ์•„์ดํ…œ ๋ชฉ๋ก์„ ๋ฐ”๋กœ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ฝ”๋“œ๋ฅผ ๋” ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

// active
{
  "id": "Column-rYZJ5DsW8WyWUrIZ-tN9p", // ํ˜„์žฌ ์•„์ดํ…œ id (useSortable ์ธ์ž๋กœ ๋„˜๊ฒผ๋˜ id)
  "data": {
    "current": {
      "sortable": {
          // ํ˜„์žฌ ์•„์ดํ…œ์ด ์†ํ•œ ์ปจํ…Œ์ด๋„ˆ ID (SortableContext์— ๋„˜๊ฒผ๋˜ id)
        "containerId": "Board-q0tzC2fGuBReTjYuzdUHL",
        // ์ปจํ…Œ์ด๋„ˆ ๋‚ด์—์„œ ํ˜„์žฌ ์•„์ดํ…œ์˜ ์ธ๋ฑ์Šค
        "index": 0,
        // ์ปจํ…Œ์ด๋„ˆ์˜ ์ „์ฒด ์•„์ดํ…œ ๋ชฉ๋ก (SortableContext์— ๋„˜๊ฒผ๋˜ items)
        "items": [
          "Column-rYZJ5DsW8WyWUrIZ-tN9p", // ํ˜„์žฌ ์•„์ดํ…œ
          "Column-uh6jGsYnCfu3peUX5ZF-O"
        ]
      },
      // ...
      "type": "column" // ํ˜„์žฌ ์•„์ดํ…œ type (useSortable ์ธ์ž๋กœ ๋„˜๊ฒผ๋˜ type)
    }
  },
  // ...
}

 

๐Ÿ’ก dnd-kit ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž์ฒด์ ์œผ๋กœ arraySwap, arrayMove ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•œ๋‹ค. arraySwap์€ ๋ฐฐ์—ด ๋‚ด ๋‘ ์š”์†Œ์˜ ์œ„์น˜๋ฅผ ๊ตํ™˜ํ•˜๊ณ , arrayMove๋Š” ์ง€์ •ํ•œ ์ธ๋ฑ์Šค๋กœ ์š”์†Œ๋ฅผ ์ด๋™์‹œํ‚จ ํ›„ ๋‚˜๋จธ์ง€ ์š”์†Œ๋ฅผ ๋ฐ€์–ด๋‚ธ๋‹ค.

import { arraySwap, arrayMove } from '@dnd-kit/sortable';

const items = ['A', 'B', 'C', 'D'];
const swapped = arraySwap(items.slice(), 1, 3); // ์ธ๋ฑ์Šค 1(B)์™€ 3(D) ๊ตํ™˜
const moved = arrayMove(items.slice(), 1, 3); // ์ธ๋ฑ์Šค 1(B)์„ ์ธ๋ฑ์Šค 3 ์œ„์น˜๋กœ ์ด๋™

console.log(swapped); // ['A', 'D', 'C', 'B']
console.log(moved); // ['A', 'C', 'D', 'B']

 

 

ํƒœ์Šคํฌ ์ˆœ์„œ ๋ณ€๊ฒฝ / ์ปฌ๋Ÿผ ๊ฐ„ ์ด๋™


์ปฌ๋Ÿผ์„ ๋„˜๋‚˜๋“œ๋Š” ํƒœ์Šคํฌ ์ด๋™์€ onDragEnd ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด A ์ปฌ๋Ÿผ(์ปจํ…Œ์ด๋„ˆ)์— ์žˆ๋˜ ํƒœ์Šคํฌ๋ฅผ B ์ปฌ๋Ÿผ์œผ๋กœ ๋“œ๋ž˜๊ทธํ•˜๋ฉด, ๋“œ๋กญํ•ด์„œ onDragEnd ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋˜๊ธฐ ์ „๊นŒ์ง„ ์–ด๋Š ์ปฌ๋Ÿผ์œผ๋กœ ์ด๋™ํ–ˆ๋Š”์ง€ ๊ฐ์ง€ํ•  ์ˆ˜ ์—†๋‹ค. ๋•Œ๋ฌธ์— ๋“œ๋กญ ์˜์—ญ์ด ๋‹ค๋ฅธ ์ปจํ…Œ์ด๋„ˆ๋ผ๋ฉด Placeholder UI๊ฐ€ ๋ Œ๋”๋ง ๋˜์ง€ ์•Š๋Š”๋‹ค.

 

UI๋ฅผ ์ฆ‰์‹œ ๋ฐ˜์˜ํ•˜๋ ค๋ฉด ํƒœ์Šคํฌ๋ฅผ ์ปฌ๋Ÿผ ์˜์—ญ์œผ๋กœ ๋“œ๋ž˜๊ทธํ•  ๋•Œ๋งˆ๋‹ค, ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ์•„์ดํ…œ ๋ชฉ๋ก์„ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์•„์ดํ…œ์ด ๋“œ๋กญ ์ปจํ…Œ์ด๋„ˆ ์œ„๋กœ ์ด๋™ํ•  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋Š” onDragOver ์ด๋ฒคํŠธ๋ฅผ ํ™œ์šฉํ•œ๋‹ค.

// use-kanban-dnd.ts
const onDragOver = ({ active, over, delta, activatorEvent }: DragOverEvent) => {
  if (!over) return; // ๋“œ๋กญ ์˜์—ญ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ
  if (active.id === over.id) return; // ๊ฐ™์€ ์œ„์น˜๋Š” ์Šคํ‚ต

  const { isActiveTask, isOverTask, isOverColumn } = getDragTypes(active, over);

  if (!isActiveTask) return; // Task ๋“œ๋ž˜๊ทธ๊ฐ€ ์•„๋‹ˆ๋ฉด ์Šคํ‚ต

  const activeSort = active.data.current?.sortable as TaskSortable;
  const overSort = over.data.current?.sortable as TaskSortable;

  const sourceTaskId = toTaskId(active.id);
  const sourceTaskIdx = activeSort.index; // ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•œ ์นด๋“œ์˜ ์ธ๋ฑ์Šค

  const sourceColumnId = activeSort.containerId;
  // ๋“œ๋กญ ์˜์—ญ์ด Task ์นด๋“œ์ด๋ฉด ํ•ด๋‹น ์นด๋“œ์˜ ์ปจํ…Œ์ด๋„ˆ๋Š” ์ปฌ๋Ÿผ์ด๋ฏ€๋กœ overSort.containerId ์—์„œ ID ํš๋“
  // ๋“œ๋กญ ์˜์—ญ์ด ์ปฌ๋Ÿผ์ด๋ฉด over ์ž์ฒด๋Š” ์ปฌ๋Ÿผ์„ ์ฐธ์กฐํ•˜๋ฏ€๋กœ over.id ์—์„œ ID ํš๋“
  const targetColumnId = isOverTask ? overSort.containerId : toColumnId(over.id);
  const targetColumn = columns[targetColumnId];

  // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ ์œ„์น˜(clientY)์™€ ์ด๋™ ๊ฑฐ๋ฆฌ(delta.y)๋ฅผ ํ•ฉ์‚ฐํ•ด์„œ ํ˜„์žฌ Y ์œ„์น˜ ๊ณ„์‚ฐ
  const currentY = (activatorEvent as MouseEvent).clientY + delta.y;

  // ๋“œ๋กญ ๋Œ€์ƒ ์นด๋“œ์˜ ์ธ๋ฑ์Šค (computeTargetTaskIdx ํ•จ์ˆ˜ ์„ค๋ช…์€ ์•„๋ž˜ ๋‚ด์šฉ ์ฐธ๊ณ )
  const targetTaskIdx = computeTargetTaskIdx({
    isOverColumn,
    targetColumn,
    overSort,
    sourceTaskId,
    currentY,
  });

  moveTask({
    sourceTaskId,
    sourceColumnId,
    targetColumnId,
    sourceTaskIdx,
    targetTaskIdx,
  });
};

 

ํƒœ์Šคํฌ๋ฅผ ๋“œ๋ž˜๊ทธํ–ˆ์„ ๋•Œ ๋“œ๋กญ ์ง€์ ์€ โถ"์ปฌ๋Ÿผ ์˜์—ญ ์œ„"(์ปฌ๋Ÿผ์— ํƒœ์Šคํฌ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ปฌ๋Ÿผ๋‚ด ๋‹ค๋ฅธ ๊ณต๊ฐ„์— ์œ„์น˜), โท"ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๋‹ค๋ฅธ ํƒœ์Šคํฌ ์œ„" ์ด๋ ‡๊ฒŒ ๋‘ ๊ฐ€์ง€ ๊ฒฝ์šฐ๋กœ ๋‚˜๋‰˜๋ฉฐ, ๊ฐ ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ด ๋‹ฌ๋ผ์ง„๋‹ค. ๋˜ํ•œ ํƒœ์Šคํฌ๋ฅผ ๋“œ๋ž˜๊ทธํ•  ๋•Œ๋งˆ๋‹ค ์•„๋ž˜ 3๊ฐ€์ง€ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•œ๋‹ค.

 

  1. ๋“œ๋ž˜๊ทธ ์•„์ดํ…œ์˜ task.columnId (๋ณ€๊ฒฝ๋œ ์ปฌ๋Ÿผ ID๋กœ ๊ต์ฒด)
  2. ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์•„์ดํ…œ์ด ์†ํ–ˆ๋˜ ์ปฌ๋Ÿผ์˜ column.taskIds
    • ๋™์ผ ์ปฌ๋Ÿผ ๋‚ด์—์„œ ๋“œ๋ž˜๊ทธํ–ˆ๋‹ค๋ฉด ์ธ๋ฑ์Šค ์ˆœ์„œ๋งŒ ๋ณ€๊ฒฝ
    • ๋‹ค๋ฅธ ์ปฌ๋Ÿผ์œผ๋กœ ๋“œ๋ž˜๊ทธํ–ˆ๋‹ค๋ฉด ํ•ด๋‹น ํƒœ์Šคํฌ ID ์ œ๊ฑฐ
  3. ๋“œ๋กญ ๋Œ€์ƒ ์ปฌ๋Ÿผ์˜ column.taskIds (์ธ๋ฑ์Šค ์ˆœ์„œ ๋ณ€๊ฒฝ)

 

์œ„ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด์„  ์†Œ์Šค ์ปฌ๋Ÿผ ID, ํƒ€๊ฒŸ ์ปฌ๋Ÿผ ID, ๋“œ๋ž˜๊ทธ ์•„์ดํ…œ(ํƒœ์Šคํฌ) ID, ์ปฌ๋Ÿผ ๋‚ด์—์„œ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•  ๋‘ ์•„์ดํ…œ(์†Œ์Šค/ํƒ€๊ฒŸ ํƒœ์Šคํฌ)์˜ ์ธ๋ฑ์Šค ์ •๋ณด๋ฅผ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.

 

์ปฌ๋Ÿผ ID ํ™•์ธ

  1. ์†Œ์Šค ์ปฌ๋Ÿผ ID: active ๊ฐ์ฒด์˜ sortable.containerId
  2. ํƒ€๊ฒŸ ์ปฌ๋Ÿผ ID:
    • ๋“œ๋กญ ๋Œ€์ƒ - ํƒœ์Šคํฌ: over ๊ฐ์ฒด์˜ sortable.containerId (ํƒœ์Šคํฌ์˜ ์ปจํ…Œ์ด๋„ˆ๋Š” ์ปฌ๋Ÿผ์ด๋ฏ€๋กœ)
    • ๋“œ๋กญ ๋Œ€์ƒ - ์ปฌ๋Ÿผ: over.id (์ด๋•Œ over ๊ฐ์ฒด๋Š” ์ปฌ๋Ÿผ์„ ์ฐธ์กฐํ•˜๋ฏ€๋กœ)

 

์•„์ดํ…œ ์ธ๋ฑ์Šค ํ™•์ธ

  1. ์†Œ์Šค ํƒœ์Šคํฌ(๋“œ๋ž˜๊ทธ ์•„์ดํ…œ): active ๊ฐ์ฒด sortable.index
  2. ํƒ€๊ฒŸ ํƒœ์Šคํฌ(๋“œ๋กญ ์˜์—ญ์— ์œ„์น˜ํ•œ ์•„์ดํ…œ)
    • ๋“œ๋กญ ๋Œ€์ƒ์ด ํƒœ์Šคํฌ์ผ ๋•Œ: over ๊ฐ์ฒด sortable.index
    • ๋“œ๋กญ ๋Œ€์ƒ์ด ์ปฌ๋Ÿผ ์˜์—ญ์ผ ๋•Œ (์ปฌ๋Ÿผ์— ์นด๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜, ์ปฌ๋Ÿผ ์œ„/์•„๋ž˜์ชฝ ์œ„์น˜)
      • [์ฒซ ์ง„์ž…์ด ์•„๋‹ ๋•Œ] ์ปฌ๋Ÿผ.์•„์ดํ…œ ๋ชฉ๋ก์— ๋“œ๋ž˜๊ทธ ์•„์ดํ…œ ID ๆœ‰ → ์กฐํšŒํ•œ ์ธ๋ฑ์Šค ๋ฐ˜ํ™˜
      • [์ฒซ ์ง„์ž…์ผ ๋•Œ] ์ปฌ๋Ÿผ.์•„์ดํ…œ ๋ชฉ๋ก์— ๋“œ๋ž˜๊ทธ ์•„์ดํ…œ ID ็„ก
        • ๋Œ€์ƒ ์ปฌ๋Ÿผ์˜ ์ฒซ ๋ฒˆ์งธ ํƒœ์Šคํฌ๋ณด๋‹ค ์œ„์ชฝ์œผ๋กœ ๋“œ๋ž˜๊ทธํ–ˆ์„ ๋•Œ: ์ฒซ ๋ฒˆ์งธ ์œ„์น˜ (์ธ๋ฑ์Šค = 0)
        • ๊ทธ ์™ธ ์ƒํ™ฉ: ๋งˆ์ง€๋ง‰ ์œ„์น˜ (์ธ๋ฑ์Šค = taskIds.length)

 

์•„๋ž˜๋Š” ํƒ€๊ฒŸ ํƒœ์Šคํฌ์˜ ์ธ๋ฑ์Šค ํ™•์ธ ๊ณผ์ •์„ ์‹œ๊ฐํ™”ํ•œ ํ”Œ๋กœ์šฐ์ฐจํŠธ

 

๋“œ๋ž˜๊ทธ ์•„์ดํ…œ Y ์ขŒํ‘œ ๊ณ„์‚ฐ

onDragOver ํ•ธ๋“ค๋Ÿฌ๋Š” delta, activatorEvent ๊ฐ์ฒด๋ฅผ ์ธ์ž๋กœ ๋ฐ›๋Š”๋‹ค. activatorEvent.clientY๋Š” ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ–ˆ์„ ๋•Œ y ์ขŒํ‘œ๋ฅผ ๋‚˜ํƒ€๋‚ด๊ณ , delta.y๋Š” ์ด๋™ํ•œ ๊ฑฐ๋ฆฌ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์ด ๋‘ ๊ฐ’์„ ๋”ํ•˜๋ฉด ํ˜„์žฌ ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์•„์ดํ…œ์˜ y ์ขŒํ‘œ๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์œ„ ์ด๋ฏธ์ง€์˜ Top Boundary๋Š” ์ฒซ ๋ฒˆ์งธ ํƒœ์Šคํฌ ์œ„์ชฝ์˜ ๊ฒฝ๊ณ„๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ๋งŒ์•ฝ ๊ณ„์‚ฐํ•œ y ์ขŒํ‘œ๊ฐ€ ์ด ๊ฒฝ๊ณ„๋ณด๋‹ค ์ž‘์œผ๋ฉด(Drag Position 1) ๋“œ๋ž˜๊ทธ ์•„์ดํ…œ์„ ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ์œผ๋กœ ์œ„์น˜์‹œํ‚ค๊ณ (์ธ๋ฑ์Šค = 0), ๊ทธ ์™ธ์—”(Drag Position 2) ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ(taskIds.length)์œผ๋กœ ์œ„์น˜์‹œํ‚จ๋‹ค.

// kanban-utils.ts
export const computeTargetTaskIdx = ({
  isOverColumn,
  targetColumn,
  overSort,
  sourceTaskId,
  currentY,
  topBoundary = 200,
}: ComputeTargetTaskIdxParams): number => {
  // ๋“œ๋กญ ๋Œ€์ƒ์ด ํƒœ์Šคํฌ์ผ ๋•Œ
  if (!isOverColumn) return overSort.index;

  // ๋“œ๋กญ ๋Œ€์ƒ์ด ์ปฌ๋Ÿผ ์˜์—ญ์ผ ๋•Œ (์ปฌ๋Ÿผ์— ์นด๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ปฌ๋Ÿผ ์œ„/์•„๋ž˜์ชฝ ์œ„์น˜)
  const index = targetColumn.taskIds.indexOf(sourceTaskId);
  // ์ปฌ๋Ÿผ ์˜์—ญ ์ง„์ž… → ์ฒซ๋ฒˆ์งธ/๋งˆ์ง€๋ง‰ ์œ„์น˜์˜ ์ธ๋ฑ์Šค๋กœ ๋Œ€์ƒ ์ปฌ๋Ÿผ์˜ ์•„์ดํ…œ ๋ชฉ๋ก์„ ์—…๋ฐ์ดํŠธํ•œ ์ƒํƒœ์—์„œ ๋‹ค์‹œ ์›€์ง์˜€์„ ๋•Œ
  if (index !== -1) return index;

  // ๋Œ€์ƒ ์ปฌ๋Ÿผ์˜ ์ฒซ๋ฒˆ์งธ ์นด๋“œ ์œ„์น˜๋ณด๋‹ค ์œ„๋กœ ๋“œ๋ž˜๊ทธ ํ–ˆ์„ ๋• ์ฒซ๋ฒˆ์งธ๋กœ, ๊ทธ ์™ธ์—” ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค๋กœ ์„ค์ •
  return currentY < topBoundary ? 0 : targetColumn.taskIds.length;
};

 

 

์„ผ์„œ ์กฐ์ •


  • ํŒจ๋‹(Panning): ํ™”๋ฉด์„ ํ„ฐ์น˜ํ•œ ์ƒํƒœ์—์„œ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ด๋™ํ•˜๋Š” ์ œ์Šค์ฒ˜
  • ํ•€์น˜์คŒ(Pinch Zoom): ๋‘ ์†๊ฐ€๋ฝ์„ ๋ชจ์œผ๊ฑฐ๋‚˜ ๋ฒŒ๋ ค์„œ ํ™”๋ฉด์„ ์ถ•์†Œ/ํ™•๋Œ€ํ•˜๋Š” ์ œ์Šค์ฒ˜

 

dnd-kit์˜ ์„ผ์„œ(sensor)๋Š” ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ƒํ˜ธ์ž‘์šฉ์„ ๊ฐ์ง€ํ•˜๊ณ  ์ œ์–ดํ•˜๋Š” ์ถ”์ƒํ™” ๋ ˆ์ด์–ด๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ Pointer, Keyboard ์„ผ์„œ๊ฐ€ ํ™œ์„ฑํ™”๋˜๋ฉฐ, useSensors ํ›…์„ ํ†ตํ•ด ๋‹ค๋ฅธ ์„ผ์„œ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. delay, distance ๊ฐ™์€ ์ œ์•ฝ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•ด์„œ ๋“œ๋ž˜๊ทธ ํ™œ์„ฑ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

๋งŒ์•ฝ ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค(listeners๊ฐ€ ํ• ๋‹น๋˜์–ด ์žˆ๋Š” ์š”์†Œ) ์•ˆ์— ๋ฒ„ํŠผ์ด ์žˆ๋‹ค๋ฉด, ํด๋ฆญํ•˜๋Š” ์ˆœ๊ฐ„ ๋“œ๋ž˜๊ทธ๊ฐ€ ํ™œ์„ฑํ™”๋ผ์„œ ๋ฒ„ํŠผ ํด๋ฆญ์ด ๋ถˆ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. distance ์ œ์•ฝ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋ชจ๋ฐ”์ผ์€ Touch ์„ผ์„œ์˜ delay, tolerance ์ œ์•ฝ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•ด์„œ ์ผ์ • ์‹œ๊ฐ„ ์ด์ƒ ํ„ฐ์น˜ํ•ด์•ผ๋งŒ ๋“œ๋ž˜๊ทธ๊ฐ€ ํ™œ์„ฑํ™”๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ๋กฑํ”„๋ ˆ์Šค ๋Œ€๊ธฐ ์‹œ๊ฐ„์€ 250ms๊ฐ€ ์ ๋‹นํ•˜๋‹ค.

 

Pointer ์„ผ์„œ๋Š” ๋ฐ์Šคํฌํ†ฑ ํด๋ฆญ๊ณผ ๋ชจ๋ฐ”์ผ ํ„ฐ์น˜ ์ด๋ฒคํŠธ๋„ ํ•จ๊ป˜ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์Šคํฌํ†ฑ, ๋ชจ๋ฐ”์ผ ๋‹ค๋ฅธ ์ œ์•ฝ ์กฐ๊ฑด์„ ์ ์šฉํ•˜๋ ค๋ฉด Mouse, Touch ์„ผ์„œ๋ฅผ ๊ฐ๊ฐ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

// ๋ฐ์Šคํฌํ†ฑ ์ตœ์ ํ™”
const mouseSensor = useSensor(MouseSensor, {
  activationConstraint: {
    // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘์„ ์œ„ํ•ด ์š”์†Œ ํด๋ฆญ ํ›„ ์ปค์„œ๋ฅผ ์ด๋™์‹œ์ผœ์•ผ ํ•˜๋Š” ์ตœ์†Œ ๊ฑฐ๋ฆฌ(px)
    distance: 10, // ํด๋ฆญ ํ›„ 10px ์ด์ƒ ์›€์ง์—ฌ์•ผ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ (์˜๋„์น˜ ์•Š์€ ํด๋ฆญ ๋ฐฉ์ง€)
  },
});

// ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”
const touchSensor = useSensor(TouchSensor, {
  activationConstraint: {
    // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘์„ ์œ„ํ•ด ํ„ฐ์น˜๋ฅผ ์œ ์ง€ํ•ด์•ผ ํ•˜๋Š” ์ตœ์†Œ ์‹œ๊ฐ„(ms)
    delay: 250, // 250ms ํ„ฐ์น˜ ์œ ์ง€ ํ•„์š” (์ผ๋ฐ˜์ ์ธ ๋ชจ๋ฐ”์ผ ์•ฑ์˜ ๋กฑํ”„๋ ˆ์Šค ๋Œ€๊ธฐ ์‹œ๊ฐ„)
    // delay ๋™์•ˆ ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ด๋™ ๊ฑฐ๋ฆฌ(px). ์ดˆ๊ณผ์‹œ ๋“œ๋ž˜๊ทธ ์ทจ์†Œ๋จ.
    tolerance: 5, // 5px ์ด๋‚ด ์›€์ง์ž„ ํ—ˆ์šฉ (์†๋–จ๋ฆผ์ด๋‚˜ ๋ฏธ์„ธํ•œ ์›€์ง์ž„ ํ—ˆ์šฉ)
  },
});

const sensors = useSensors(touchSensor, mouseSensor);

return <DndContext sensors={sensors} >

 

touch-action CSS ์†์„ฑ์€ ํ„ฐ์น˜ ๊ธฐ๋ฐ˜ ์ž…๋ ฅ ์žฅ์น˜์—์„œ ํŠน์ • ์š”์†Œ๊ฐ€ ์–ด๋–ค ๊ธฐ๋ณธ ํ„ฐ์น˜ ๋™์ž‘(ํŒจ๋‹, ํ•€์น˜์คŒ ๋“ฑ)์„ ์ˆ˜ํ–‰ํ• ์ง€ ๊ฒฐ์ •ํ•œ๋‹ค. ํ„ฐ์น˜ ์„ผ์„œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด touch-action: manipulation์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•˜๊ณ  ์žˆ๋‹ค.

.draggable-item {
  /* ํŒจ๋‹/ํ•€์น˜์คŒ์€ ํ—ˆ์šฉํ•˜์ง€๋งŒ ๋”๋ธ”ํƒญ ํ™•๋Œ€ ๊ฐ™์€ ๋น„ํ‘œ์ค€ ์ œ์Šค์ฒ˜๋Š” ๋น„ํ™œ์„ฑํ™”ํ•˜์—ฌ ํ„ฐ์น˜ ๋“œ๋ž˜๊ทธ ๋ฐ˜์‘์„ฑ ๊ฐœ์„  */
  touch-action: manipulation;
  /* ํ…์ŠคํŠธ ์„ ํƒ ๋ฐฉ์ง€ */
  user-select: none;
  /* ํ…์ŠคํŠธ ์„ ํƒ ๋ฐฉ์ง€ for iOS Safari */
  -webkit-user-select: none;
  /* ํ…์ŠคํŠธ ์„ ํƒ ๋ฐฉ์ง€ for Firefox */
  -moz-user-select: none;
}

 

์ฐธ๊ณ ๋กœ Pointer ์„ผ์„œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ ๋“œ๋ž˜๊ทธํ•  ๋•Œ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ๋™์ž‘์œผ๋กœ ์ธํ•ด ํ™”๋ฉด๋„ ํ•จ๊ป˜ ์Šคํฌ๋กค๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ๋“œ๋ž˜๊ทธํ•  ์š”์†Œ์— touch-action: none ์„ ์„ค์ •ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ณธ ํ„ฐ์น˜ ๋™์ž‘์„ ๋น„ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•œ๋‹ค. (์ฐธ๊ณ  ๋งํฌ)  

 

 

์ด์Šˆ ํ•ด๊ฒฐ


ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์—๋Ÿฌ

 

Next.js๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด Warning: Prop aria-describedby did not match… ๊ฐ™์€ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ์‹œ ์ƒ์„ฑํ•œ DndContext ID์™€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ƒ์„ฑํ•œ ID๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ. ๋ฆฌ์•กํŠธ useId ํ›…์„ ์ด์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ID๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. (์ฐธ๊ณ  ์ด์Šˆ #926)

import { useId } from 'react';
const id = useId()

return <DndContext id={id} />

 

๋ฌดํ•œ ๋ฃจํ”„

 

์•„์ดํ…œ์„ ์—ฌ๊ธฐ์ €๊ธฐ ๋“œ๋ž˜๊ทธํ•˜๋‹ค ๋ณด๋ฉด ๊ฐ€๋” Maximum update depth exceeded(์ปดํฌ๋„ŒํŠธ ๋ฌดํ•œ ๋ฃจํ”„) ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์ด ์—๋Ÿฌ๋Š” Sortable ํ”„๋ฆฌ์…‹์„ ์‚ฌ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•œ๋‹ค. onDragOver ํ•ธ๋“ค๋Ÿฌ์— ์žˆ๋Š” ์ƒํƒœ ๋ณ€๊ฒฝ ํ•จ์ˆ˜์— 0ms ๋””๋ฐ”์šด์Šค๋ฅผ ์ ์šฉํ•˜๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. (์ฐธ๊ณ  ์ด์Šˆ #900)

// use-kanban-dnd.ts
import { useDebouncedCallback } from 'use-debounce';

const moveTask = useKanbanStore.use.moveTask();
const debouncedMoveTask = useDebouncedCallback(moveTask, 0);

// ...
const onDragOver = ({ active, over, delta, activatorEvent }: DragOverEvent) => {
  // ...

  debouncedMoveTask({
    sourceTaskId,
    sourceColumnId,
    targetColumnId,
    sourceTaskIdx,
    targetTaskIdx,
  });
};

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•