일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 여행 플래너
- 발리
- 발리숙소
- 나트랑 여행
- 깜란 리조트
- 발리 다이빙
- 괌 남부투어
- ATM 출금
- 나트랑 리조트
- 야마다야 료칸 후기
- 유후인 여행
- 나트랑
- 회고
- 괌 여행지
- 개발기
- 발리 아메드
- 괌 관광지
- 캐나다워홀
- 발리 여행
- 유후인 야마다야 료칸
- 발리 숙소
- 발리여행
- 발리 가성비 호텔
- 나트랑 마사지
- 유후인 료칸
- 캐나다라이프
- 후쿠오카
- 미아리조트
- 아메드 다이빙
- 샌달스레스토랑
- Today
- Total
히히낙락
MVP 개발기 #3 | react-beautiful-dnd 본문
#2에서는 검색 플로우에 대해 설명했었습니다.
이번에 말씀드릴 내용은 드래그 & 드롭 정렬에 대한 건데요.
현재 개발한 플래너에는 드래그 & 드롭 정렬이 구현되어 있습니다. 사용한 모듈은 react-beautiful-dnd 인데요.
구현하는 과정은 그렇게 beautiful 하지 않았습니다. 크게 이유는 두 가지인데요.
우선. 해당 기능을 구현하기 위해서 필수적으로 필요한 구조는 아래와 같습니다.
소스코드로 표현하면 아래와 같습니다.
import {
DragDropContext,
DragUpdate,
DropResult,
Draggable,
Droppable
} from "react-beautiful-dnd";
<DragDropContext
onDragEnd={(result : DropResult) => endAction(result)}
onDragUpdate={(result: DragUpdate) => updateAction(result)}>
<Droppable droppableId={"someting uniqueKey #1"}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{provided.placeholder}
<Draggable
index={index}
draggableId={"somthing uniqueKey #2"}>
{(provided2) => (
<div ref={provided2.innerRef} {...provided2.dragHandleProps} {...provided2.draggableProps}></div>
)}
</Draggable>
// ... <Draggable>...
</div>
)}
</Droppable>
</DragDropContext>
드래그 & 드롭을 구현하는 컴포넌트 구성이 일단 복잡합니다. 컴포넌트를 정리하면 아래와 같습니다.
DragDropContext.
Droppable에서 발생하는 이벤트 핸들링 컴포넌트입니다. children에 Droppable, Draggable이 존재해야 합니다.
Droppable.
드롭 영역 (droppableId는 고유해야 합니다.)
Draggable.
드래그 가능한 영역 (draggableId는 고유해야 합니다.)
그럼. Droppable과 Draggable의 children 인 provided와 snapshot 은 뭘까요?
우선 provided를 간략하게 설명하자면 HTML Element입니다.
그래서 Droppable, Draggable 내부의 children 은 HTMLElement 여야만 해요.(Droppable > Draggable 이 안됩니다.)
snapshot은 뭘까요. 스냅숏은 드래그/드롭 이벤트의 상태를 뜻합니다.
기본 구현은 이렇습니다. 위의 예제대로 진행한다면 드래그&드롭 기능 구현은 가능하나 저는 아쉬운 부분이 있었습니다.
기본적으로 react-beautiful-dnd의 Drop 영역은 {provided.placeholder}에서 영역을 잡아놓기 때문에 밀려 보이는 효과가 있습니다.
하지만 저는 Drag 시 Drop 될 영역을 미리 표시하고 싶었습니다.
이 기능을 구현하기 위한 결과는 이렇습니다.
type PlaceholderProps = {
clientHeight: 0;
clientWidth: 0;
clientX: 0;
clientY: 0;
};
const [placeholderProps, setPlaceholderProps] =
useState<PlaceholderProps>(defaultPlaceholder);
const onDragUpdate = (update: DragUpdate) => {
if (!update.destination) {
return;
}
const draggableId = update.draggableId;
const destinationIndex = update.destination.index;
const domQuery = `[${queryAttr}='${draggableId}']`;
const draggedDOM = document.querySelector(domQuery);
if (!draggedDOM) {
return;
}
const { clientHeight, clientWidth } = draggedDOM;
const clientY =
parseFloat(
window.getComputedStyle(draggedDOM.parentNode).paddingTop
) +
[...draggedDOM.parentNode.children]
.slice(0, destinationIndex)
.reduce((total, curr) => {
const style =
curr.currentStyle || window.getComputedStyle(curr);
const marginBottom = parseFloat(style.marginBottom);
return total + curr.clientHeight + marginBottom;
}, 0);
setPlaceholderProps({
clientHeight,
clientWidth,
clientY: clientY - clientHeight - {padding 값},
clientX: parseFloat(
window.getComputedStyle(draggedDOM.parentNode).paddingLeft
),
});
};
return (
<DragDropContext
onDragEnd={handleDragEnd}
onDragUpdate={onDragUpdate}
>
<Droppable
droppableId={"something uniqueKey #1"}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
<Draggable
index={index}
draggableId={"somthing uniqueKey #2"}>
{(provided2) => (
<div ref={provided2.innerRef} {...provided2.dragHandleProps} {...provided2.draggableProps}></div>
)}
</Draggable>
{provided.placeholder}
<div
className="drop-placeholder"
style={{
top: placeholderProps.clientY,
left: placeholderProps.clientX,
height: placeholderProps.clientHeight,
width: placeholderProps.clientWidth,
}}
/>
</div>
)}
</Droppable>
</DragDropContext>
)
설명하자면 커스텀한 Drop 영역을 표현하기 위해 실시간으로 provided.placeholder의 현재 위치를 추적하고 상태값에 저장합니다.
저장한 상태값은 렌더 시 커스텀한 Drop 영역의 style 값을 수정합니다.
(물론 drop-placeholder의 스타일은 추가로 작성해주셔야 합니다.)
이처럼 간단한 Skeleton Drop 영역을 구현한다고 굉장한 소스코드가 컴포넌트 내에 구현된 것이 개인적으로는 불만이었습니다.
드래그가 종료되어 드롭되었을 때. 변경된 정렬 기준으로 DB를 업데이트해야 합니다. 그래서 자연스레 onDropEnd 이벤트 발생 시 새로 정렬된 전체 목록이 올 줄 알았는데요. 전달되는 정보는 드래그 대상 ID, 드롭 한 대상의 ID 만 전달됩니다. 그래서 변경된 상태로 상태 관리를 진행하기 위해서는 사용자가 직접 re-sort를 진행해야 했었는데요. 관련 로직은 아래와 같습니다.
const reorder = (list: Array<VisitPlace>, result: DropResult) => {
if (result.destination) {
const newItems = [...list];
const [removed] = newItems.splice(result.source.index - 1, 1);
newItems.splice(result?.destination.index - 1, 0, removed);
return newItems;
} else {
return list;
}
};
const handleDragEnd = async (result: DropResult) => {
const reSortedItems = reorder(sortedItem, result);
setSortedItem(reSortedItems);
};
이렇게 구현하여 드래그 & 드롭 정렬 이후 DB 반영까지 진행할 수 있었습니다.
서비스는 여기서 확인하실 수 있어요.
'개발 > 여행 플래너 (가제)' 카테고리의 다른 글
MVP 개발기 #2 | google-place-api (0) | 2023.03.14 |
---|---|
MVP 개발기 #1 | 서비스 소개 (0) | 2023.03.14 |