효뚜르팝의 Dev log
dnd kit의 useDraggable를 사용하여 드래그 가능한 컴포넌트 구현하기 본문
해결해야하는 문제
회사에서 계산기 컴포넌트가 어떠한 위치로든 드래그가 가능한 기능이 구현 가능하도록 세팅해야했다.
기존에 drag and drop이 가능한 목차를 dnd-kit의 useSortable을 사용하여 구현한 경험이 있어서, 이번에도 dnd-kit에서 제공하는 useDraggable을 사용하여 구현하기로 하였다.
참고) dnd kit - usedraggable 공식 문서
https://docs.dndkit.com/api-documentation/draggable/usedraggable
문제 해결 과정
처음에 구현했던 방식
1. 드래그를 사용하는 컴포넌트를 DndContext로 감싸줌
2. 드래그를 사용할 계산기 컴포넌트에서 useDraggable hook에서 제공하는 setNodeRef를 ref로 연결시킴
3. 드래그를 활성화할 버튼에는 setActivatorNodeRef를 연결시키고 listeners와 attributes 속성을 넣어줌
3. useDraggable의 transform을 style에 적용하여 드래그를 할 때 이동할 수 있도록 처리함
// 계산기 컴포넌트의 부모 컴포넌트 (버튼 클릭 시 컴포넌트가 화면에 노출이 됨)
const CalculatorButton = () => {
return(
// 이하 생략
<DndContext>
<Calculator
onClose={handleCalculatorClose}
/>
</DndContext>
)
}
const Calculator = () => {
const { attributes, listeners, setNodeRef, transform, setActivatorNodeRef } =
useDraggable({
id: 'calculator',
});
return(
<div
ref={setNodeRef}
style={{
transform: CSS.Translate.toString(transform)
}}
>
<div
{...listeners}
{...attributes}
>
<IconButton ref={setActivatorNodeRef}> 드래그 버튼 </IconButton>
</div>
</div>
)
}
위의 방식은 공식 문서에 있는 그대로 코드를 따라한 방식인데, 있는 그대로 코드를 따라했는데 의도한대로 동작하지 않는 이슈가 있었다.
드래그를 하는 중에는 컴포넌트가 마우스를 따라서 이동하다가, 마우스를 놓는 순간 다시 제자리로 돌아갔다.
아래와 같은 방식으로 구현했는데 제대로 동작하지 않아서, 무언가 내가 빠뜨리고 있는 것 같다는 생각이 들었다.
github이나 stackoverflow, chatgpt 를 활용해서 drag로 변경된 위치에 대해서 검색해보았는데,
생각보다 useSortable를 활용한 칸반보드를 만드는 예시는 많은데 (심지어 유튜브 강의도 있음) useDraggable에 대한 예시는 거의 찾아보기 어려웠다.
다행히 삽질 중 단비같은 글 하나를 발견했다.
https://stackoverflow.com/questions/74671519/how-to-handle-free-dragn-drop-with-dnd-kit
How to handle free drag'n drop with dnd-kit?
I'm trying to create a simple sticky notes app by using @dnd-kit. I've found a lot of sortable examples, but can't find any free drag'n-drop examples. I've created a simple codesandbox to help you
stackoverflow.com
이 글의 질문자도 마찬가지로 useSortable에 관한 정보는 많은데 useDraggable에 대한 정보가 많지 않아서 기능 구현을 하는 데에 어려움을 겪고 있었다.
다행히 답변자분께서 예시 코드를 제공해주어서 이를 참고해서 해결할 수 있었다.
참고한 코드 : https://codesandbox.io/s/dnd-kit-free-drag-n-drop-forked-gj9qmt
dnd-kit-free-drag-n-drop (forked) - CodeSandbox
dnd-kit-free-drag-n-drop (forked) by jlamb6 using @dnd-kit/core, react, react-dom, react-scripts
codesandbox.io
요지는, onDragEnd에서 위치를 저장하기 위한 추가적인 처리가 필요한 것이었다.
onDragEnd에서 위치를 저장하고 이를 update하는 로직이 누락되어있었기 때문에 onDragEnd가 실행되자마자 드래그 위치가 초기로 리셋되었던 것!
이를 참고해서 수정한 코드는 아래와 같다.
const INITIAL_POSITION = {
bottom: 88,
right: 118,
};
const CALCULATOR_WIDTH = 324;
const CALCULATOR_HEIGHT = 575;
// 계산기 컴포넌트의 부모 컴포넌트 (버튼 클릭 시 컴포넌트가 화면에 노출이 됨)
const CalculatorButton = () => {
const [position, setPosition] = useState({
x: screenWidth - INITIAL_POSITION.right - CALCULATOR_WIDTH,
y: screenHeight - INITIAL_POSITION.bottom - CALCULATOR_HEIGHT,
});
function handleDragEnd(event: DragEndEvent) {
if ('calculator' !== event.active.id) return;
position.x += event.delta.x;
position.y += event.delta.y;
const newPosition = { x: position.x, y: position.y };
setPosition(newPosition);
}
return(
// 이하 생략
<DndContext onDragEnd={handleDragEnd}>
<Calculator
onClose={handleCalculatorClose}
positionStyle={{
right: `${screenWidth - position.x - CALCULATOR_WIDTH}px`,
bottom: `${screenHeight - position.y - CALCULATOR_HEIGHT}px`,
}}
/>
</DndContext>
)
}
const Calculator = () => {
const { attributes, listeners, setNodeRef, transform, setActivatorNodeRef } =
useDraggable({
id: 'calculator',
});
const transformStyle = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: {};
return(
<div
ref={setNodeRef}
style={{
...positionStyle,
...transformStyle,
}}
>
<div
{...listeners}
{...attributes}
>
<IconButton ref={setActivatorNodeRef}> 드래그 버튼 </IconButton>
</div>
</div>
)
}
추가된 로직
1. 이동되는 위치를 저장하는 state를 추가함 ( position )
2. onDragEnd 이벤트가 발생할 때 실행되는 handleDragEnd에서 계산기의 위치를 계산하고 이를 position state에 저장하는 로직을 추가함
3. 자식 컴포넌트인 Caculator 컴포넌트에게 props로 position을 내려주어서, 드래그가 종료되었을 때도 이동된 위치로 고정되도록 스타일을 적용할 수 있도록 처리
참고로, 계산기의 초기 위치가 left와 bottom을 기준으로 정해져있고, dnd-kit에서는 계산기의 x축과 y축의 값을 제공하고 있어서 window에서 제공하는 innerHeight와 innerWidth를 사용해서 계산기의 위치를 계산했다.
- right 위치 값 : 스크린 너비 - (x축의 위치) - (계산기 고정 너비값)
- bottom 위치 값 : 스크린 높이 - (y축의 위치) - (계산기 고정 높이값)
결과
onDragEnd에서 위치를 저장하는 로직을 추가하고, 위치를 전달받은 계산기 컴포넌트에서 이를 style에 적용하니 의도한대로 동작하는 것을 확인할 수 있었다!

'TIL' 카테고리의 다른 글
| [Type-Challenges] Pick, Readonly, Tuple to Object, First of Array, Length of Array, Length of Tuple, Exclude (3) | 2024.06.09 |
|---|---|
| Solving TypeScript Errors (1) | 2024.05.21 |
| [유데미(udemy) - React CleanCode] Hooks (0) | 2024.05.02 |
| [유데미(udemy) - React CleanCode] Components (2) | 2024.04.29 |
| [유데미(udemy) - React CleanCode] Props (2) | 2024.04.14 |