Stack/REACT

[React_Library] 간편하게 적용하는 drag & drop (hello-pangea/dnd)

sstaar_mx 2025. 4. 4. 13:28

 

이미 다양한 웹에서 너무나도 익숙하게 사용하고 있는 기능이 있다.

순서가 있는 여러 리스트에서 마우스로 특정 요소를 끌어서 순서를 원하는대로 변경하는 기능이다.

 

사실 굳이 마우스를 이용해서 끌고 놓고 하지 않고 버튼을 이용해서 순서를 바꾸는 방식으로 간단히 구현할 수 있다.


리스트 순서 변경하기 (기본편)

구현 환경 : React, Typescript, MUI, emotion (css)

 

우선 내가 해당 기능을 구현할 때 고려했던 환경에 대해서 그림과 함께 알아보자

 

화면에 보여지는 리스트가 있고, 해당 리스트의 순서를 변경할 때는 모달을 띄워서 순서를 변경하도록 했다.

그래서 보여지는 리스트는 그대로 있고, 모달 안에서만 순서 변경이 이루어져야 했다.

그 이후 '저장' 버튼을 누르면 변경된 순서가 리스트에 반영이 되어야 했다.

 

또한 가장 첫번째 리스트와 마지막 리스트는 한 방향으로만 이동이 가능하도록 구현해야 했다.

그리고 사용자가 움직인 리스트가 어떤 것인지도 표시해줘야 했다.

 

기능 구현에 필요한 조건들을 세우고 그를 바탕으로 코드를 작성했다.

 

const [settingList, setSettingList] = useState([]);  // 리스트 관리 state
const [moveItem, setMoveItem] = useState(-1);  // 움직인 아이템을 체크하는 state

상태값을 2개를 사용했다.

 

먼저로는 페이지에서 전달해준 리스트와 같은 데이터지만 동일하게 반영되면 안되기에

모달 내에서 해당 리스트 값을 관리하는 상태값이 있다.

두번째로는 현재 이동한 리스트가 어떤 것인지 확인하기 위한 상태값이 있다.

 

const handleOrder = (idx: number, type: string) => {
  const curArr = [...settingList];

  if (type === "up") {
    [curArr[idx - 1], curArr[idx]] = [curArr[idx], curArr[idx - 1]];
    setMoveItem(idx - 1);
  } else {
    [curArr[idx], curArr[idx + 1]] = [curArr[idx + 1], curArr[idx]];
    setMoveItem(idx + 1);
  }

  setSettingList(curArr);
};

해당 함수는 특정 리스트에서 위 혹은 아래 버튼을 선택하면 해당 리스트가 움직일 수 있도록 구현했는데

내가 구현한 리스트에서는 바로 위 혹은 바로 아래와 순서 변경만 가능했기에

인덱스값을 이용해 자리만 변경하는 형태로 구현했다.

 

구현한 화면을 보면 리스트 오른쪽에 아이콘으로 위, 아래를 표시했고

움직이게 되면 움직인 요소의 색을 변경해 사용자의 편의성도 고려했다.

 

이 정도로 만족할 수도 있지만 만약 가장 아래있는 리스트를 가장 상단으로 올려야 할 때,

다소 불편한 부분도 있을 수 있어 드래그 & 드롭 방식으로 변경하기로 결정했다.

 

리스트 순서 변경하기 (희망편)

버튼으로 순서를 변경하는 것은 간단하게 구현하기 좋지만, 사용자 측면에서 여러가지 불편한점이 존재한다.

그래서 이런 점을 해소하기 위해 드래그 & 드롭 기능을 붙이면 깔끔하게 처리할 수 있다.

 

드래그 & 드롭 기능을 구현하도록 돕는 라이브러리는 많이 있는데, 그 중에서 간편하게 적용할 수 있는 라이브러리를 찾아왔다.

 

 

@hello-pangea/dnd

Beautiful and accessible drag and drop for lists with React. Latest version: 18.0.1, last published: 2 months ago. Start using @hello-pangea/dnd in your project by running `npm i @hello-pangea/dnd`. There are 175 other projects in the npm registry using @h

www.npmjs.com

해당 라이브러리는 드래그 & 드롭을 가장 간편하게 사용할 수 있는 라이브러리다.

사실 이 라이브러리의 전신이라고 할 수 있는 라이브러리가 있다.

 

 

react-beautiful-dnd

Beautiful and accessible drag and drop for lists with React. Latest version: 13.1.1, last published: 3 years ago. Start using react-beautiful-dnd in your project by running `npm i react-beautiful-dnd`. There are 2101 other projects in the npm registry usin

www.npmjs.com

react-beautiful-dnd 라는 라이브러리는 Jira 개발사로 유명한 Atlassian에서 만든 라이브러리인데

2021년부터 유지보수가 중단이 된 상황이고, 해당 라이브러리를 포크해서 유지보수 버전으로 만든게

hello-pangea/dnd 라이브러리가 되겠다.

 

간단하게 라이브러리 맛 보기

해당 라이브러리를 사용하기 위해서는 몇 가지 컴포넌트에 대해서 알아야 한다.

 

<DragDropContext />

해당 컴포넌트는 드래그 & 드롭이 동작할 수 있는 환경을 제공하는 컴포넌트가 되겠다.

interface Responders {
  // optional
  onBeforeCapture?: OnBeforeCaptureResponder;
  onBeforeDragStart?: OnBeforeDragStartResponder;
  onDragStart?: OnDragStartResponder;
  onDragUpdate?: OnDragUpdateResponder;

  // required
  onDragEnd: OnDragEndResponder;
}

Props를 보면 드래그 액션에 따라서 여러가지 동작을 할 수 있도록 구성 되어있어서,

드래그 동작에 대한 부가적인 함수는 해당 컴포넌트에 연결해 주면 되겠다.

 

<Droppable />

해당 컴포넌트는 실제적으로 드래그 & 드롭을 할 수 있는 영역이 되겠다.

interface Props {
  // required
  droppableId: DroppableId;
  children: (DroppableProvided, DroppableStateSnapshot) => ReactNode;
  // optional
  mode?: DroppableMode;
  type?: TypeId;
  isDropDisabled?: boolean;
  isCombineEnabled?: boolean;
  direction?: Direction;
  renderClone?: DraggableChildrenFn | null;
  ignoreContainerClipping?: boolean;
  getContainerForClone?: () => HTMLElement;
}

type DroppableMode = 'standard' | 'virtual';
type Direction = 'horizontal' | 'vertical';

이번에도 해당 컴포넌트의 Props를 살펴보면 반드시 설정해야 하는 건 droppableId와 children이 있다.

droppableId와 children은 자식 요소와 관련이 되어있는데 이 부분은 아래 예시코드를 통해 확인해 보자.

 

<Droppable droppableId="droppable-1" type="PERSON">
  {(provided, snapshot) => (
    <div
      ref={provided.innerRef}
      style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
      {...provided.droppableProps}
    >
      <h2>I am a droppable!</h2>
      {provided.placeholder}
    </div>
  )}
</Droppable>;

예시코드를 보면 droppableId에 값이 설정되어 있는데, 해당 값은 자식 요소에 동일하게 연결해 줘야 한다.

그 과정을 {...provided.droppableProps}에서 처리해 주고 있다.

또한 children에서 제공되는 provided의 innerRef 역시 자식 요소의 ref에 연결해 줘야 한다.

 

실제 사용할 때도 이 두 가지 부분은 놓치지말고 적용해 줘야 한다.

 

<Draggable />

해당 컴포넌트는 드래그 할 특정 요소를 감싸는 컴포넌트가 되겠다.

interface Props {
  // required
  draggableId: DraggableId;
  index: number;
  children: ChildrenFn;
  // optional
  isDragDisabled?: boolean;
  disableInteractiveElementBlocking?: boolean;
  shouldRespectForcePress?: boolean;
}

Props를 살펴보면 draggableId, index, children이 반드시 필요한 값이 되겠다.

아마 draggableId 역시 Droppable 컴포넌트에서 활용하는 용도와 같은 용도이지 않을까 생각해 본다.

 

라이브러리 실전 적용 하기

<DragDropContext onDragEnd={handleDragEnd}>
  <Droppable droppableId="components" direction="vertical">
    {(provided) => (
      <S.listBox {...provided.droppableProps} ref={provided.innerRef}>
        {settingList.map((el, idx) => {
          const resultTitle = titleList[el.type];

          return (
            <Draggable draggableId={String(el.id)} index={idx} key={el.id}>
              {(provided) => (
                <S.listWrap {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
                  <div>{resultTitle}</div>
                  <Button variant="plain" size="sm" color="danger">삭제</Button>
                </S.listWrap>
              )}
            </Draggable>
          );
        })}
        {provided.placeholder}
      </S.listBox>
    )}
  </Droppable>
</DragDropContext>

적용 컴포넌트를 보면 이렇게 구성되어 있다.

DragDropContext로 환경을 감싸주고 onDragEnd라는 메서드에 특정 함수를 연결해

드래그 액션이 끝났을 때 특정 동작을 하도록 설정했다.

 

그 안에 Droppable 컴포넌트를 넣고 자식 요소로 listBox 컴포넌트를 넣었다.

내가 해당 라이브러리를 적용하면서 이해한 바로는 Droppable 컴포넌트나 Draggable 컴포넌트는

드래그 & 드롭을 할 수 있는 환경에서 그 안에 자식 요소들이 어떤 역할을 한다 라고 정해주는 컴포넌트라고 생각한다.

 

그래서 요소의 스타일을 적용할 경우에는 자식 요소에 정해주면 되고

제공되는 컴포넌트는 환경에 따라 원하는 대로 동작할 수 있도록 도와주는 컴포넌트라고 이해했다.

 

  const handleDragEnd = (result: DropResult) => {
    if (!result.destination) return;

    const items = [...settingList];
    const [reorderedItem] = items.splice(result.source.index, 1);
    items.splice(result.destination.index, 0, reorderedItem);

    setSettingList(items);
  };

드래그 액션이 끝났을 때 적용하는 함수로 handleDragEnd 를 썼는데 해당 함수의 로직은 위와 같다.

 

결과물

버튼으로 이동할 때 보다 훨씬 정갈하고 깔끔한 UI/UX가 나왔다.

여기서 조금 더 발전 시킨다면 내가 방금 이동한 요소를 시각적으로 표시할 수 있으면 좋겠지만

뭐 거기까지는 욕심인것 같아 여기까지만 하려고 한다.

 


포스팅 작성에 참고한 감사한 글들

- hello-pangea/dnd : Github readme

- IBBI 님 블로그 글 : @hello-pangea/dnd를 사용하여 Drag and Drop 해보자