React

dnd-kit 세상에서 가장 아름다운 DnD(드래그 앤 드랍)

이요시야 2023. 8. 4. 16:48

업무 중,

드래그 앤 드랍(dnd)을 구현해야 하는 일이 생겼다.

 

좋아 보이는 react-beautiful-dnd

npm trends를 보아하니

무려, Atlassian에서 지원하는 react 전용 react-beautiful-dnd가 여러모로 좋아보였다.

 

하지만! 구현을 마치고 나니 react-beautiful-dnd에는 큰 문제가 있었다. (전혀 beautiful 하지 않았다,,,)

그것은 바로 flex-wrap과 grid 형태의 dnd를 지원하지 않는 것이었다!

 

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

해당 깃헙 이슈인데, 나를 포함한 수많은 개발자들의 분노를 느낄 수 있었다.

라이브러리 개발자분의 코멘트를 보면, 앞으로도 지원하지 않을 것 같다. ^---^

분노가 담긴 코멘트ㅎㅎ

 

 

결국, 구글링 끝에 dnd-kit을 사용하게 되었다.

아직 dnd-kit와 관련된 한글 문서가 없어, 프로덕트에 적용하는데 애가 좀 있었지만.

사용하고 나니 dnd-kit이야말로 beautiful 하다:)

 

그래서 DnD를 구현하기 위해, dnd-kit을 사용하려는 분들의 시간을 조금이라도 아끼드리려,

사용하며 느낀 아주 중요한 포인트들만 남기려 한다. 그러니 공식 Api도 같이 보셔야 한다.

부디 의미가 있길!


1.  DndContext - Droppable - Draggable

dnd-kit의 사용을 위한 가장 중요한 개념이다.

dnd-kit을 사용하기 위한 태그는 계층 구조로 되어야 한다.

<DndContext>
	<Droppable>
		<Draggable>
		</Draggable>
	</Droppable>
</DndContext>

 

DndContext - 드래그 앤 드랍을 사용을 위한 기본 영역이다.

Droppable - 드래그 한 아이템을 가져다 놓을 수 있는 공간이다.

Draggable - 드래그할 아이템을 감싸줘야 한다.

 

하지만 우리의 문제는 늘 이렇게 단순하지 않다.

 

#. 정렬을 위해서는!!!

드래그한 아이템을 특정 공간에 넣는 간단한 DnD는 위의 태그들만 사용하여 구현 가능하다.

 

하지만, 대부분의 DnD는 정렬 가능한 리스트를 위하여 사용할 것이다.

이때는 dnd-kit에서 제공하는 sortable을 사용해야 한다.

> npm install @dnd-kit/sortable

 

sortable을 사용하면 위의 구조가 조금 바뀌는데,

DndContext - SortableContext - Droppable - Draggable의 구조가 된다.

 

갑자기 SortableContext가 중간에 끼게 되는데,

<SortableContext items={배열} strategy={원하는 정렬 방식}>

해당 태그에 DnD기능을 원하는 배열을 넣어주면 된다.

이렇게하면, SortableContext 영역이 DnD가 가능한 컨테이너가 된다. 이 SortableContext는 여러 개일 수 있다.

 

※ 중요한 점은 dnd-kit이 여러 개인 SortableContext를 구분하는 방법이 그 안에 들어간 배열로 구분한다는 것이다.

 

또 기억해야하는 것은 Sortable 구조에서 사용되는 태그는 <DndContext>와 <SortableContext> 두 개뿐이다.

위에 기본 구조에서는 <DndContext>, <Droppable>, <Draggable> 세 개의 태그를 사용하였는데,

<Droppable>과 <Draggable> 태그는 사용하지 않게 된다.

 

다만, useDroppable과 useSortable을 사용해서 드래그 받을 영역과(Droppable)과 드래그 할 영역(Draggable)을 표시해줘야 한다.

useDroppable과 useSortable에 해당하는 내용은 공식 API 문서에서 보시길!

 

 

 

2. 직접 배열을 업데이트해줘야 한다.

정말 중요한 부분이다. 개인적으로 이 부분에서 시간을 많이 썼다.

공식 API 문서를 다 읽고, 위의 구조대로 코드를 작성 했는데도, 드래그만 동작할 뿐 드래그로 뒤바뀐 정렬이 저장되지 않았다.

알고보니, 이 부분을 개발자가 직접 로직을 짜줘야 했다.

 

사실 dnd-kit에서,

아이템을 마우스로 누르고 드래그를 이곳저곳으로 하면, 미세한 픽셀 단위마다 리렌더링이 계속 일어난다. 콘솔을 찍어보면 알 수 있다.

이때, 무수히 많은 리렌더 속에서 SortableContext는 자신의 속성으로 들어온 items(배열)을 계속 관찰하면서 정렬을 유지한다.

그러니 아무리 드래그를 해도 자신에게 들어온 items(배열)이 바뀌지 않으니 순서는 바뀌지 않았던 것이다.

 

그러니 내가 드래그한대로, 순서가 바뀌려면 드래그한 아이템을 Droppable 영역에 놓았을 때 원본 배열도 그 순서로 바뀌어야 한다. 

 

포인트는 <DndContext> 태그에서 onDragOver와 onDragEnd 속성이다.

onDragOver 속성은 마우스가 다른 아이템이나 컨테이너 위에 올라갔을 때를 감지하고, onDragEnd 속성은 드래그가 끝났을 때를 감지한다.

이 속성 안에 자기가 짠 로직 함수를 넣어주면 된다.

 

요약하자면,

드래그 중(onDragOver)과 끝났을 때(onDragEnd), 리렌더가 계속 되는데, 이때마다 원본 배열이 내가 의도한 순서대로 있어야 한다. 이 코드는 개발자가 직접 짜줘야 한다.

 

하단의 codesandbox에 쉽게 구현이 되어있으니 꼭 참고하시길!

https://codesandbox.io/s/playground-0mine?file=/src/components/SortableItem.jsx

 

 

 

3. SortableContext는 컨텍스트일 뿐 공간이 아니다.

말그대로 SortableContext는 물리적 공간이 아니다. 따라서, 자식으로 컴포넌트에 Droppable의 영역을 설정해줘야 한다.

<Droppable> 태그를 사용할 필요는 없고, useDroppable을 사용해 얻어진 ref를 Droppable을 희망하는 컴포넌트에 주입해주면 된다.

const { setNodeRef } = useDroppable({ id: 블라블라 })

<SortableContext>
	<드랍퍼블원하는 컴포넌트 ref={setNodeRef} id={id}>
	</드랍퍼블원하는 컴포넌트>
</SortableContext>

 

 

4.DragOverlay는 원본 아이템이 아닌 미리보기이다.

아름다운 미리보기 효과를 위해 <DragOverlay> 태그를 사용하면 된다.

다만, 이해해야 하는 점은 <DragOverlay> 태그에 자식으로 주는 컴포넌트는 원본이 아니라는 점이다.

해당 기능을 사용하면 드래그할 때 투명하게 미리보기로 보이는 것이 사실 원본이다.

 

예를들어, 드래그 하는 아이템에 체크박스에 체크가 되어있다고 치자.

그러면 내가 그 아이템을 누르는 순간 체크박스 된 원본이 투명하게 되고, 체크박스 풀린 것이 원본 행세를 한다.

그러니 <DragOverlay>에 주입하는 아이템을 항상 원본가 같은 컨디션을 유지시켜줘야 한다.

 

 

 

5. 특정 부분을 눌러야 드래그 가능하게 할 때

드래그 대상 아이템과 내가 누를수 있는 영역이 다르게 할 수 있다.

 

※ 예를 들면 큰 배너 안에 있는 화살표 버튼을 눌러야 드래그가 되게 할 때

const { setNodeRef, listeners } = useSortable({ id: 블라블라 })

useSortable을 호출해서 얻어진 setNodeRef와 listeners가 있다.(물론 더 많이 있다.)

배너 컴포넌트에는 setNodeRef를 주입해주고,

화살표 컴포넌트에는 listeners를 주입해주면 된다.

그 방법은 공식 API 문서에!

 

 

6. 그 외

센서와 충돌 알고리즘이 있다.

 

DndContext에 넣어주는 센서는 Api 문서에 나온대로 적어주면 된다. 

해당 코드로 키보드 등 여러 동작이 인식되는 모양이다.

 

또한, DndContext에 collisionDetection이라는 속성이 있는데,

드래그를 인식하는 알고리즘이다. 나의 경우에는 정밀한 인식을 원해서 pointerWithin값을 넣어줬다.

<DndContext collisionDetection={pointerWithin} >

 

개인적으로 dnd-kit 사용감은 매우 훌륭했다. 개인적으로 다시 사용할 가치가 있게 느껴진다.

이상하거나 변경된 부분은 댓글로 알려주세요!