히히낙락

MVP 개발기 #3 | react-beautiful-dnd 본문

개발/여행 플래너 (가제)

MVP 개발기 #3 | react-beautiful-dnd

히히낙락 0.0 2023. 3. 14. 18:02
반응형

#2에서는 검색 플로우에 대해 설명했었습니다.

이번에 말씀드릴 내용은 드래그 & 드롭 정렬에 대한 건데요.

 

드래그 & 드롭 정렬

 

현재 개발한 플래너에는 드래그 & 드롭 정렬이 구현되어 있습니다. 사용한 모듈은 react-beautiful-dnd 인데요.

구현하는 과정은 그렇게 beautiful 하지 않았습니다. 크게 이유는 두 가지인데요.

 

우선. 해당 기능을 구현하기 위해서 필수적으로 필요한 구조는 아래와 같습니다.

출저. react-beautiful-dnd git

소스코드로 표현하면 아래와 같습니다.

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 이 안됩니다.)

provided 안에 ref, 와 Element 의 props 들이 존재합니다.

snapshot은 뭘까요. 스냅숏은 드래그/드롭 이벤트의 상태를 뜻합니다. 

 

snapshot 내부 구현입니다.
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