[React] react-beautiful-dnd 으로 요소 드래그 되도록 만들기

2022. 4. 3. 17:57WEB Dev/Javascript | REACT | Node.js

728x90

 

 

요소를 드래그 해 순서를 바꾸는 기능을 구현하고 있다.

드래그 앤 드랍은 HTML5에서 기본적으로 API를 제공하고 있다.

하지만 해당 기능을 일일이 적용해서 드래그 기능을 만들게 된다면 시간도 오래걸리고 어렵기 때문에 라이브러리를 사용한다. 

 

엄청 다양한 라이브러리가 있고 여러 라이브러리를 테스트해봤는데 atlassian 의 react-beautiful-dnd 라이브러리가 가장 단순해서 적용하기 쉬웠었다.

하지만 실제로 내 프로젝트에 적용하기가 너무 어려워서 고전을 겪고 있다. 일단 순서를 바꿔야 하는 정보들을 map으로 돌려서 렌더링 하고 있고, 중첩을 지원하지 않는 다양한 제약이 있긴 하다.

그래서 다시 한 번 차근차근 개념에 대해 생각해보면서 글을 적으면서 만들어보고 성공이냐 실패냐를 정해야 할 거 같다.

 

 

 


 


 

 

내가 만들어야 하는 것은 Goal 컴포넌트 사이를 왔다갔다 하면서 순서를 변경해야 하고, todo 컴포넌트들 사이를 왔다갔다 해주어야 한다. 

이 과정에서 엄청 다양한 오류가 생기고 있는데 이것을 하나하나 풀어내야 한다.

 

 

 

 

 


React-beautiful-dnd

 

react-beautiful-dnd 공식 문서

 

 

GitHub - atlassian/react-beautiful-dnd: Beautiful and accessible drag and drop for lists with React

Beautiful and accessible drag and drop for lists with React - GitHub - atlassian/react-beautiful-dnd: Beautiful and accessible drag and drop for lists with React

github.com

 

 

 

설치하기

 

 

# yarn
yarn add react-beautiful-dnd

# npm
npm install react-beautiful-dnd --save

 

 

나는 yarn을 통해 설치했다. 

 

 

그리고 사용을 원하는 컴포넌트에 아래 패키지를 import 해준다.

 

 

 

 

간단하게 구조를 보자면 아래 그림과 같다.

 

https://github.com/atlassian/react-beautiful-dnd

 

DragDropContext

 

import { DragDropContext } from 'react-beautiful-dnd';

 

위의 DragDropContext로 내가 드래그 할 컴포넌트의 전체를 감싸야 한다. 래핑한다고 표현한다.

 

이 DragDropContext는

- onDragEnd (필수 props)

- onDragUpdate

- onDragEnd

 

위의 3개 Props를 사용할 수 있다.

onDragEnd 함수는 드래그가 끝나면 실행될 함수다.

 

<DragDropContext onDragEnd={함수}></DragDropContext>

 

 

Droppable

 

DragDropContext안에는 Droppable이 존재해야 한다. 끌어서 드롭될 수 있는 영역을 말한다.Droppable은 droppable한 구역을 구분하기 위해 droppableId를 가져야 한다.

 

그리고 하위 children이 함수 형태여야 한다. 

그래서 (provided, snapshot) => React.ReactElement 형태의 함수를 사용한다.

provided에는 드래그 앤 드롭이 작동하기 위한 props들이 들어있기 때문에 children에 할당해주어야 한다.

 

 

{provided.placeholder}

 

Droppable 할 구역의 닫는 태그 위에 {provided.placeholder}를 꼭 넣어주어야 한다.

이것이 draggable 드래그 할 영역을 만들어 내는 코드다.

 

 

 

Draggable

 

드래그 하고 싶은 아이템에 Draggable 속성을 넣어주어야 한다.

이 Draggable은 반드시 Droppable 안에 들어가야 한다.

 

Draggable의 필수 props는 아래와 같다.

 

- ref={provided.innerRef}

- {...provided.draggableProps}

- {...provided.dragHandleProps}

 

 

이 DragDropContext, Droppable, Draggable 세 가지가 반드시 필요하고 이것을 내 프로젝트에 적용해보면 아래와 같다.

 

 

 

현재 구조가 저렇게 생겼고 배열로 데이터를 받아서 map으로 렌더링 하고 있는데, 

Goal 컴포넌트와 todo 컴포넌트가 별도로 생성되어 있다.

우선 DragDropContext 부터 차례로 적용해보자

 

 

 

1. DragDropContext로 감싸기

 

드래그 기능을 적용할 컴포넌트는 OrderItem 컴포넌트다.

OrderItem 컴포넌트는 단독 페이지이기 때문에 return 으로 렌더링되는 가장 바깥에 <DragDropContext>를 넣어준다.

onDragEnd props는 반드시 필요하기 때문에 임시로 콘솔에 "드래그"라고 표시되는 함수를 우선 작성한다.

 

 

import { DragDropContext } from 'react-beautiful-dnd'

 

 

상단에 추가해주고,

<DragDropContext> </DragDropContext>로 감싸준다.

 

 

  /* 함수 선언 시작 */

  const onDragEnd = () => {
    console.log("드래그")
  }

  /* 함수 선언 종료 */

return (
    <DragDropContext onDragEnd={onDragEnd}>
        <div className="goals-list-wrap">
          {todoDataArray.map((data, idx) => {
            return <OrderTodoGoal data={data} index={index} key={data.goalId} />
          })}
        </div>
    </DragDropContext>
)

 

 

2. Droppble 로 감싸기

 

 

todoDataArray는 내가 사용하는 중첩 배열이다. map을 통해서 정보를 전달해주고 있다.

Goal은 OrderTodoGoal이라는 컴포넌트를 반환하고 있다.

이 OrderTodoGoal을 각각 드래그 할 수 있어야 하기 때문에 DragDropContext 바로 아래에 Dropabble 영역을 만들어준다.

 

 

 

import {DragDropContext, Droppable} from "react-beautiful-dnd";

 

Droppable을 추가해주고,

<Droppable>를 아래 추가할 것인데 드래그 아이템이 왔다갔다 할 부분을 설정해 주는 droppableId를 반드시 설정해주어야 하는데 이 droppabledId는 문자여야 한다.

 

A setup problem was encountered.> Invariant failed: Draggable[id: goals]: requires an integer index prop

셋업 문제가 발생했습니다.> 불변 실패: 드래그 가능 [id: 목표]: 정수 인덱스 프로포트가 필요합니다.

 

  /* 함수 선언 시작 */

  const onDragEnd = () => {
    console.log("드래그")
  }

  /* 함수 선언 종료 */

return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="Goal">
        <div className="goals-list-wrap">
          {todoDataArray.map((data, idx) => {
            return <OrderTodoGoal data={data} index={index} key={data.goalId} />
          })}
        </div>
      </Droppable>
    </DragDropContext>
)

 

 

그리고 Droppable 안에 provided를 뿌려주고 Droppable안에 들어갈 요소들에게 provided를 적용해준다.

 

  /* 함수 선언 시작 */

  const onDragEnd = () => {
    console.log("드래그")
  }

  /* 함수 선언 종료 */

return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="Goal">
      {provided =>  (
        <div className="goals-list-wrap" {...provided.droppableProps} ref={provided.innerRef}>
          {todoDataArray.map((data, idx) => {
            return <OrderTodoGoal data={data} index={index} key={data.goalId} />
          })}
        </div>
      )}
      </Droppable>
    </DragDropContext>
)

 

 

그리고 필요하다고 한 {provided.placeholder} 를 넣어준다.

 

 

  /* 함수 선언 시작 */

  const onDragEnd = () => {
    console.log("드래그")
  }

  /* 함수 선언 종료 */

return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="Goal">
      {provided =>  (
        <div className="goals-list-wrap" {...provided.droppableProps} ref={provided.innerRef}>
          {todoDataArray.map((data, idx) => {
            return <OrderTodoGoal data={data} index={index} key={data.goalId} />
          })}
          {provided.placeholder}
        </div>
      )}
      </Droppable>
    </DragDropContext>
)

 

그리고 내부에 돌아가는 OrderTodoGoal에는 순서를 표기하기 위해 id 값이 필요하다. 

나에게는 goalId라는 정보가 있기 때문에 (순서의 역할도 함) 해당 값을 id로 넘겨준다.

 

  /* 함수 선언 시작 */

  const onDragEnd = () => {
    console.log("드래그")
  }

  /* 함수 선언 종료 */

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="Goal">
      {provided =>  (
        <div className="goals-list-wrap" {...provided.droppableProps} ref={provided.innerRef}>
          {todoDataArray.map((data, idx) => {
            return <OrderTodoGoal data={data} index={index} key={data.goalId} />
          })}
          {provided.placeholder}
        </div>
      )}
      </Droppable>
    </DragDropContext>
  )

 

 

 

3. Draggable 적용하기

 

 

그리고 드래그 될 OrderTodoGoal에게 Draggable 을 설정해준다.

 

 

import {DragDropContext, Droppable, Draggable} from "react-beautiful-dnd";

 

Draggable 추가해주고

 

OrderTodoGoal 컴포넌트에 Draggable을 추가할 것이다.

처음에 map으로 리턴되는 <OrderTodoGoal /> 에다가 Draggable을 감싸고 provided를 적용하려고 했는데 HTMLElement 가 아니면 안된다는 에러 메시지가 뜬다.

 

index.js:1 react-beautiful-dnd A setup problem was encountered.> Invariant failed: provided.innerRef has not been provided with a HTMLElement. You can find a guide on using the innerRef callback functions at:

index.js:1 react-beautiful-dnd 설정 문제가 발생했습니다.> 불변성 실패: 제공.innerRef에 HTMLement가 제공되지 않았습니다. innerRef 콜백 함수 사용에 대한 가이드는 다음 URL에서 찾을 수 있습니다.

 

위 에러에 도움이 될 만한 레퍼런스

 

Invariant failed: provided.innerRef has not been provided with a HTMLElement. · Issue #2002 · atlassian/react-beautiful-dnd

Building a Chrome extension using React trying to implement the most basic setup with dnd but running into this issue, tried multiple times with minimum requirements no luck. Tried both 'innerR...

github.com

 

 

그렇기 때문에 OrderTodoGoal이 렌더링 되고 있는 return 부분에다가 <div> 같은 HTML 로 한 번 감싸주고 Draggable을 추가해주어야 한다.

 

 

{todoDataArray.map((data, idx) => {
    return <OrderTodoGoal data={data} index={index} key={data.goalId} />
})}

 

이 부분을

 

 

{todoDataArray.map((data, idx) => {
return ( 
        <div>
        <OrderTodoGoal data={data} index={index} key={data.goalId} />
        </div>
      )
    }
})}

 

 

이렇게 바꿔주고 Draggable을 감싸준다.

 

 

{todoDataArray.map((data, index) => {
    return (
      <Draggable>
       {provided => (
          <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
            <OrderTodoGoal data={data} index={index}  id={data.goalId} />
          </div>
        )}  
      </Draggable>
    )
})}

 

 

 

감싼 div에 provided props들을 여러 개 지정해주어야 하는데, 

ref={provided.innerRef}, {...provided.draggableProps}, {...provided.dragHandleProps} 총 세 개를 같이 넣어준다. 

 

{todoDataArray.map((data, index) => {
    return (
      <Draggable draggableId={String(index)} index={index} key={data.goalId}>
       {provided => (
          <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
            <OrderTodoGoal data={data} index={index}  id={data.goalId} />
          </div>
        )}  
      </Draggable>
    )
})}

 

 

전체 코드를 확인하면 아래와 같다.

 

 

  /* 함수 선언 시작 */

  const onDragEnd = () => {
    console.log("드래그")
  }

  /* 함수 선언 종료 */

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="Goal">
      {provided =>  (
        <div className="goals-list-wrap" {...provided.droppableProps} ref={provided.innerRef}>
          {todoDataArray.map((data, index) => {
            return (
              <Draggable draggableId={String(index)} index={index} key={data.goalId}>
               {provided => (
                  <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
                    <OrderTodoGoal data={data} index={index}  id={data.goalId} />
                  </div>
                )}  
              </Draggable>
            )
          })}
          {provided.placeholder}
        </div>
      )}
      </Droppable>
    </DragDropContext>
  )

 

 

draggableId의 경우 문자열이 되어야 하고 map으로 돌고 있기 때문에 String으로 index를 문자열로 바꿔서 넣어준다.

 

 

이렇게까지 만들면 드디어 드래그가 되기 시작한다.

다만 아직 onDragEnd의 함수가 완성되지 않아 드래그 후 드랍하면 고정되지는 않는다.

 

나는 OrderTodoGoal 아래에 다시 Todo가 움직이는 구조를 만들어야 해서 여기까지 우선 작업하고 하단의 Todo 부분에 동일한 방식으로 react-beautiful-dnd를 적용해야 한다.

 

 

 

굴러가유~

 

728x90