[nextJS] @hello-pangea/dnd를 사용하여 Drag and Drop 해보자

IBBI·2024년 8월 2일
3

nextJS

목록 보기
5/5

@hello-pangea/dnd

결과부터 냅다 넣는 나 제법 한국사람다워요 :O

팀 프로젝트 FeedB 개발 과정에서 이미지 DND 구현을 담당하게 되었다.
그래서 검색했을 때 가장 많이 나오는 react-beautiful-dnd 라이브러리를 이용하려 했다.

하지만 아틀라시안에서 react-beautiful-dnd 라이브러리를 더 이상 지원하지 않았고 react 18과도 호환이 안되서 이슈가 발생한다는 말이 있어서
react-beautiful-dnd를 계승하는 @hello-pangea/dnd 라이브러리로 대체하게 되었다.

typescript도 지원하며 문법은 react-beautiful-dnd와 거의 같기 때문에 react-beautiful-dnd 문법도 참고하면서 개발을 진행했다.

@hello-pangea/dnd 깃허브 링크



💻 설치

# npm
npm install @hello-pangea/dnd --save

🍀 컴포넌트 이해하기

3가지 컴포넌트를 활용하여 DND가 이뤄진다.

간단히 설명해보자면,
1️⃣ DrapDropContext : DND의 상태를 제공해준다고 보면 될 것 같다. 여기서 onDragEnd 등 다양한 DND 이벤트를 등록 해준다.
2️⃣ Droppable : DROP할 수 있는 영역으로 명시한다.
3️⃣ Draggable : DRAG할 수 있게 컴포넌트를 감싸준다.



✨ 코드

1. import

import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";

2. DND를 가능하게 할 범위 설정하기

DragDropContext 컴포넌트 안에 Droppable 컴포넌트를 넣어준다.
Droppable은 아이템을 drop하는 영역으로, 아이템이 여러 개일 수 있기 때문에 droppableId를 넣어줘야 한다.

const DndContainer = () => {
  const [showImageUrlList, setShowImageUrlList] = useState<ImageType[]>([]);
  const handleDragEnd = () => {}; // 뒤에서 설명
  
  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="cardLists" direction="horizontal">
        {provided => (
          <div className="cardLists" {...provided.droppableProps} ref={provided.innerRef}>
           // 드래그 할 컴포넌트를 넣을 영역
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

export default DndContainer;

⚡️ 주의 ⚡️

1️⃣ DroppabledroppableIdprovided를 리턴하는 html elementclassName 일치시켜줘야 한다.
2️⃣ provided를 리턴해주는 element는 컴포넌트가 아니라 html element로 작성해주어야 한다. (컴포넌트를 사용하고 싶다면 div 태그 안에 작성해주면 됨)

3. 드래그 할 컴포넌트 넣기

Draggable은 아이템을 drag 하는 영역이다.
draggableIdindex, 자식 요소를 필요로 하며, 자식 요소는 함수를 사용한다.

const DndContainer = () => {

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="cardLists" direction="horizontal">
        {provided => (
          <div className="cardLists" {...provided.droppableProps} ref={provided.innerRef}>
            {showImageUrlList.map((e, i) => (
              <Draggable draggableId={e.id} index={i} key={e.id}>
                {provided => (
                    <div
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                      ref={provided.innerRef}
                    >
                      // 원하는 컴포넌트 넣기
                    </div>
                  )}
              </Draggable>
            ))}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

⚡️ 주의 ⚡️

1️⃣ DraggabledraggableIdkey가 같아야 한다.
<Draggable draggableId<={`test-${e.id}`} index={i} key={`test-${e.id}`}>
2️⃣ Droppable과 마찬가지로 html elemnent를 이용해 props를 넘겨주어야 한다. (그렇지 않으면 동작하지 않음)

4. provided.placeholder

컴포넌트 이동시 자리 배열이 알맞게 위치하도록 placeholder 함수를 넣어줘야 하며, 넣지 않으면 placeholder가 없다는 에러 메세지가 뜬다.

 return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="cardLists" direction="horizontal">
        {provided => (
          <div className="cardLists" {...provided.droppableProps} ref={provided.innerRef}>
            {showImageUrlList.map((e, i) => (
              <Draggable draggableId={e.id} index={i} key={e.id}>
                {provided => (
                    <div
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                      ref={provided.innerRef}
                    >
               		// 원하는 컴포넌트 넣기
                    </div>
                  )}
              </Draggable>
            ))}
            {provided.placeholder} 
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

5. 이동한 배열을 저장하는 함수 추가

드래그를 끝내고 드롭을 했을 때의 결과를 저장해야 리렌더링시에도 같은 데이터를 유지할 수 있기 때문에 handleChange라는 다음의 함수를 만들어 DragDropContextonDragEnd props로 넘겨준다.

import { DropResult } from "@hello-pangea/dnd";

const DndContainer = () => {
  // result -> DND를 완료했을 때 라이브러리에서 제공하는 정보
  const handleDragEnd = (result: DropResult) => {
	// item을 유효한 위치에 drop하지 않은 경우 종료
    if (!result.destination) return;
    
    const items = Array.from(showImageUrlList);
    const [reorderedItem] = items.splice(result.source.index, 1); // 드래그한 항목을 자르고 그 항목을 변수에 저장
    items.splice(result.destination.index, 0, reorderedItem); // reorderedItem를 드롭한 위치에 삽입
    
    setShowImageUrlList(items);
  };

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
		// ...
    </DragDropContext>
  );
};

export default DndContainer;

간단히 말해, 드래그한 항목을 출발 위치에서 제거하고, 드롭한 위치에 삽입한 다음, 상태를 업데이트하여 변경된 배열을 반영하는 것!




📎 참고 사이트
https://github.com/LeeHyungGeun/react-beautiful-dnd-kr
https://velog.io/@deli-ght/react-beautiful-dnd-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95
https://bepyan.github.io/blog/dnd-master/6-react-beautiful-dnd#google_vignette
https://kasterra.github.io/react-beautiful-dnd-1/
https://velog.io/@real-bird/React-Drag-and-Drop

profile
IBBI의 말하는 감자 탈출 프로젝트

1개의 댓글

comment-user-thumbnail
2025년 7월 8일

덕분에 간단하게 드래그 앤 드랍 적용했습니다~
좋은 글 감사합니다!

답글 달기