본문 바로가기

React.js

[React] react-beautiful-dnd를 이용한 Drag and Drop 구현

웹 디자인 도구나 레이아웃 빌더를 개발하면서 사용자가 요소들을 자유롭게 이동하고 그룹화할 수 있는 인터페이스의 필요성을 느꼈습니다. 이를 위해 react-beautiful-dnd 라이브러리를 선택하여 직관적이고 부드러운 드래그 앤 드롭 경험을 구현했습니다.

 

react-dnd가 아니라 react-beautiful-dnd를 선택한 이유가 무엇인가요?

react-dnd는 react-beautiful-dnd보다 자유도가 높고, 커스텀하기에도 훨씬 유리하다는 장점이 있습니다. 하지만 위치 변경 애니메이션이나 드래그 후 이동 로직을 직접 정의해야 한다는 단점이 있었습니다.

저는 이번 과제에서 복잡한 커스텀 작업이 필요하지 않았고, 단순히 위아래로 요소를 이동하면서 기본적인 애니메이션이 자연스럽게 적용되는 라이브러리가 더 적합하다고 판단했습니다. 이러한 이유로 직관적이고 간편하게 사용할 수 있는 react-beautiful-dnd를 선택하게 되었습니다.

 

구조

DragDropContext

  • DragDropContext은 Drag and Drop이 일어나는 전체영역이다.
  • 필수 함수:
    • onDragStart: 드래그가 시작될 때 실행되는 함수. (Optional)
    • onDragEnd: 드래그가 끝날 때 실행되는 함수. (Required)
<DragDropContext>
      <Droppable droppableId="dndlists">
        {provided => (
          <anyOtherComponent className="dndlists" {...provided.droppableProps} ref={provided.innerRef}>
           // 여기에 움직일 컴포넌트를 넣어줄 예정
          </anyOtherComponent> 
        )}
      </Droppable>
    </DragDropContext>

 

Droppable

  • 드롭 가능한 영역을 정의하는 컴포넌트입니다.
  • 필수 속성:
    • droppableId: 각 드롭 영역의 고유 ID를 정의합니다.
 <DragDropContext onDragEnd={handleChange}>
      <Droppable droppableId="dndlists">
        {provided => (
          <div className="dndlists" {...provided.droppableProps} ref={provided.innerRef}>
            ✅
            {post.map((e: any, i: number) => (
              {/* Draggable 컴포넌트가 들어갑니다 */}
            ))}
          </div>
        )}
      </Droppable>
    </DragDropContext>

 

Droggable

  • 드래그 가능한 항목을 정의하는 컴포넌트입니다.
  • 필수 속성:
    • draggableId: 각 드래그 항목의 고유 ID를 정의합니다.
    • index: 드래그 항목의 위치를 나타냅니다.
<DragDropContext onDragEnd={handleChange}>
      <Droppable droppableId="dndlists">
        {provided => (
          <div className="dndlists" {...provided.droppableProps} ref={provided.innerRef}>
            {post.map((e: any, i: number) => (
              <Draggable draggableId={`test-${e.id}`} index={i} key={`test-${e.id}`}>
                {(provided, snapshot) => {
                  return (
                    <div
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                      ref={provided.innerRef}
                    >
                      // 원하는 컴포넌트 넣어주기
                    </div>
                  );
                }}
              </Draggable>
            ))}
          </div>
        )}
      </Droppable>
    </DragDropContext>

 

🔥 Draggable 의 draggableId와 key가 같아야 한다.

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

 

함수

 

onDragStart

  • 드래그가 시작될 때 호출됩니다.
  • 반환 값:
    • source.droppableId: 시작 영역의 ID.
    • source.index: 시작 영역의 인덱스.
 

onDragEnd

  • 드래그가 끝날 때 호출됩니다. 필수로 정의해야 합니다.
  • 반환 값:
    • source: 드래그 시작 위치 정보.
    • destination: 드래그 종료 위치 정보.
    • reason: 드래그 종료 이유.

 

실제 구현 사례

프로젝트 구조

프로젝트는 크게 세 가지 주요 컴포넌트로 구성했습니다.

  • ElementComponent: 개별 요소 렌더링
  • LayerComponent: 레이어 패널 및 요소 관리
  • ViewportComponent: 작업 영역 및 요소 배치

 

핵심 코드 분석

 

1. Droppable 영역 설정

<Droppable droppableId="viewport" direction="horizontal">
  {(provided) => (
    <ViewportContainer 
      {...provided.droppableProps} 
      ref={provided.innerRef}
    >
      {/* 요소들 렌더링 */}
      {provided.placeholder}
    </ViewportContainer>
  )}
</Droppable>

 

2. Draggable 요소 구현

<Draggable 
  key={element.id}
  draggableId={element.id}
  index={index}
>
  {(provided) => (
    <ViewportItem
      ref={provided.innerRef}
      {...provided.draggableProps}
      {...provided.dragHandleProps}
    >
      <ElementComponent 
        type={element.type}
        isSelected={selectedElementIndex.includes(actualIndex)}
      />
    </ViewportItem>
  )}
</Draggable>

 

주요 기능

  1. 요소 생성: 사용자가 div, span, p 태그를 동적으로 추가 가능
  2. 요소 선택: Shift 키를 이용한 다중 선택 지원
  3. 그룹화: 요소들을 그룹으로 묶어 함께 관리

 

상태 관리 전략

  • elements: 전체 요소 배열
  • groups: 요소 그룹 정보
  • selectedElementIndex: 현재 선택된 요소 인덱스

 

드래그 종료 핸들러

const onDragEnd = (result) => {
  const { source, destination } = result;

  // 유효하지 않은 드래그 처리
  if (!destination) return;

  // 요소 재정렬 로직
  const newElements = Array.from(elements);
  const [reorderedItem] = newElements.splice(source.index, 1);
  newElements.splice(destination.index, 0, reorderedItem);

  // 상태 업데이트
  setElements(newElements);
};

 

학습 및 개선 포인트

  1. 초기에는 복잡한 드래그 앤 드롭 로직에 어려움을 겪었습니다.
  2. 그룹화와 다중 선택 기능 구현에 많은 시간을 투자했습니다.
  3. 성능 최적화를 위해 React.memo()와 같은 최적화 기법 적용

 

느낀 점

react-beautiful-dnd는 간단한 드래그 앤 드롭 기능부터 복잡한 인터랙티브 인터페이스까지 다양하게 활용할 수 있는 강력한 라이브러리입니다. 하지만 최신 React(18) 기능과의 완전한 호환성 보장이 어렵고, 현재 적극적인 유지보수가 되고 있지 않아서 장기적인 프로젝트에서는 다른 대안을 고려해야 합니다.