데브코스

[react-beautiful-dnd] drag and drop 구현

hxx_1 2024. 11. 7. 16:02

1. DragDropContext 태그 

: 드래그 앤 드롭을 가능하게 허용할 부분을 감싸주는 태그

: props 속성에는 onDragStart 와 onDragEnd(필수) 가 있다.

: onDragEnd 에는 요소를 드래그 한 후 드롭했을 때 실행할 함수를 지정한다. 

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1> Final Space Characters</h1>
        <DragDropContext onDragEnd={handleEnd}>
         ...
        </DragDropContext>
      </header>
    </div>
  );

 

2. onDragEnd 에 지정할 함수 작성

 const handleEnd = (result)  => {
    //result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함. 
    console.log(result);

    // 목적지가 없으면 이 함수를 종료합니다.
    if(!result.destination) return;

    // 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
    const items = Array.from(characters);

    // 1. 변경시키는 아이템을 배열에서 지워주기
    // 2. return 값으로 지워진 아이템을 잡아주기
    const [reorderedItem] = items.splice(result.source.index,1);

    // 원하는 자리에 reorderedItem 을 insert 해준다.
    items.splice(result.destination.index, 0, reorderedItem);
    setCharacters(items);
  }

1. Array.from() 을 사용하여 원본 배열을 복사하고 items 라는 변수에 저장한다.

2. splice 메소드를 이용하여 변경시키는 아이템의 위치에 해당하는 값을 배열(item)에서 삭제하고, 해당 값을 reorderedItem 에 저장한다. 

3. 변경시키고자 하는 위치에 reorderedItem 의 값을 삽입한다. splice() 메소드에서 두 번째 매개변수가 0이면 어떤 요소도 삭제하지 않고 새로운 요소를 삽입만 한다. 

4. 위와 같은 과정을 수행하면 드래그 앤 드랍을 통해 아이템의 위치를 변경할 수 있다. 

 

splice 메소드

array.splice(startIndex, deleteCount, item1, item2, ...)
  • startIndex: 수정을 시작할 인덱스 위치 (필수)
  • deleteCount: 제거할 요소의 개수 (선택)
  • item1, item2, ...: 추가할 새로운 요소들 (선택)
  • splice() 는 원본 배열을 직접 수정하며 제거된 요소들을 새로운 배열로 반환한다. 

➡️ 궁금했던 점 

 

Q. 어차피 받고 싶은 값은 하나인데 왜 굳이 구조분해할당을 이용해서 값을 저장할까?

const reorderedItem = items.splice(result.source.index,1); 이렇게 해도 되는거 아닌가?

 

A. 구조 분해 할당을 사용하는 이유는 splice () 메서드가 항상 배열을 반환하기 때문이다. 

< 예시> 

const fruits = ['apple', 'banana', 'orange'];

// 일반적인 할당
const removed = fruits.splice(1, 1);
console.log(removed); // ['banana'] (배열)
console.log(typeof removed); // object (배열)

// 구조분해 할당
const [removedItem] = fruits.splice(1, 1);
console.log(removedItem); // 'banana' (문자열)
console.log(typeof removedItem); // string

 

<차이점 설명>

1. 일반 할당

const reorderedItem = items.splice(result.source.index, 1);
// reorderedItem은 ['item'] 형태의 배열

 

2. 구조분해 할당

const [reorderedItem] = items.splice(result.source.index, 1);
// reorderedItem은 'item' 형태의 단일 값

 

3. Droppable 태그

: 드롭이 가능한 부분을 감싸주는 태그

: droppableId => 구분자(필수 태그)

<Droppable droppableId="characters">
  {(provided) => (/* ... */)}
</Droppable>

드롭 가능한 영역을 정의한다. 고유 id 를 characters 로 설정한다. 

 

Q. provided 는 무엇인가?

A. provided는 Droppable 컴포넌트의 children으로 전달되는 함수의 매개변수이다. react-beautiful-dnd 라이브러리가 내부적으로 이 provided 객체를 생성하여 전달한다. 

<ul
  className="characters"
  {...provided.droppableProps}
  ref={provided.innerRef}
>
  • provided.droppableProps: 드래그 앤 드롭에 필요한 속성들
  • ref={provided.innerRef}: DOM 요소 참조를 위한 ref
{characters.map(({ id, name }, index) => {
  return (/* Draggable 컴포넌트 */);
})}

characters 배열 매핑 :  각 캐릭터를 드래그 가능한 요소로 변환한다.

 

4. Draggable 태그 

: draggableId  => 구분자 (필수 속성)

: index => 정렬을 위한 데이터

: Draggable 이 내부 함수에 전달해주는 인자 => provided

<Draggable key={id} draggableId={id} index={index}>
  {(provided) => (/* ... */)}
</Draggable>
  • key: React의 리스트 렌더링을 위한 고유 키
  • draggableId: 드래그 요소의 고유 ID
  • index: 드래그 요소의 순서
<li
  ref={provided.innerRef}
  {...provided.draggableProps}
  {...provided.dragHandleProps}
>
  <p>{name}</p>
</li>
  • provided.draggableProps: 드래그 기능을 위한 기본적인 속성들을 포함
// 실제로는 이런 속성들이 자동으로 추가됨
<li
  data-rbd-draggable-context-id="0"
  data-rbd-draggable-id="task-1"
  style={{ 
    position: 'relative',
    transform: 'translate(0px, 0px)'
  }}
  onTransitionEnd={...}
>
  • provided.dragHandleProps: 실제 드래그 동작을 처리하는 이벤트 핸들러들을 포함
// 실제로는 이런 이벤트 핸들러들이 자동으로 추가됨
<li
  onMouseDown={...}
  onKeyDown={...}
  onTouchStart={...}
  tabIndex={0}
  aria-grabbed="false"
>

5. provided.placeholder

{provided.placeholder}
  • 드래그 중인 아이템의 공간을 유지하기 위한 요소
  • 드래그 중인 아이템이 position: fixed로 설정되어 원래 위치에서 벗어나면서 생기는 공간을 채워준다. 
  • 반드시 드롭 영역의 마지막에 위치해야 한다. 

 

전체 코드

import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";

const finalSpaceCharacters = [
  {
    id: "gary",
    name: "Gary Goodspeed",
  },
  {
    id: "cato",
    name: "Little Cato",
  },
  {
    id: "kvn",
    name: "KVN",
  },
];
function App() {
  const [characters, setCharacters] = useState(finalSpaceCharacters);
  const handleEnd = (result)  => {
    //result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함. 
    console.log(result);

    // 목적지가 없으면 이 함수를 종료합니다.
    if(!result.destination) return;

    // 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
    const items = Array.from(characters);

    // 1. 변경시키는 아이템을 배열에서 지워주기
    // 2. return 값으로 지워진 아이템을 잡아주기
    const [reorderedItem] = items.splice(result.source.index,1);

    // 원하는 자리에 reorderedItem 을 insert 해준다.
    items.splice(result.destination.index, 0, reorderedItem);
    setCharacters(items);
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1> Final Space Characters</h1>
        <DragDropContext onDragEnd={handleEnd}>
          <Droppable droppableId="characters">
            {(provided) => (
              <ul
                className="characters"
                {...provided.droppableProps}
                ref={provided.innerRef}
              >
                {characters.map(({ id, name }, index) => {
                  return (
                    <Draggable key={id} draggableId={id} index={index}>
                      {(provided) => (
                        <li
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                        >
                          <p>{name}</p>
                        </li>
                      )}
                    </Draggable>
                  );
                })}
                {provided.placeholder}
              </ul>
            )}
          </Droppable>
        </DragDropContext>
      </header>
    </div>
  );
}

export default App;

 


🌟 배운 점

오늘은 react-beautiful-dnd 라는 라이브러리를 사용하여 드래그 앤 드랍 기능을 구현해봤다. 라이브러리를 사용하니 훨씬 간단하게 사용할 수 있었던 반면에, 이해가 안되는 부분들이 많아서 많이 찾아봤다. 검색을 해보면서 코드 한 줄 한 줄 이해하려고 노력했다. 드래그 앤 드랍 기능은 모든 웹사이트에서 사용하는 것을 쉽게 볼 수 있으니 구현하는 방법을 잘 기억해두고 써먹어야겠다. 그리고 드래그 앤 드랍을 구현할 수 있는 다른 방법들도 찾아봐야겠다.