현재 개발중인 프로젝트에서 드래그 앤 드롭으로 아이템의 순서를 바꾸거나 삭제하기 위해 기능을 직접 js(ts)로 구현하려다가 시간이 오래 걸릴 듯 하여 외부 라이브러리를 사용하기로 했다.
React-beautiful-dnd라는 이름의 라이브러리를 설치해주었다.
<DragDropContext />
: 드래그 앤 드롭을 가능하게 허용할 부분을 감싸주는 태그onDragEnd
: 드래그를 끝낸 시점에 호출되는 함수 (필수 속성)<Droppable />
: 드롭이 가능한 부분을 감싸주는 태그droppableId
: 구분자 (필수 속성)provided
ref={provided.innerRef} {...provided.droppableProps}
속성을 부여해줘야 한다<Draggable />
: 드래그가 가능한 부분을 감싸주는 태그draggableId
: 구분자 (필수 속성)index
: 정렬을 위한 데이터 (number)provided
provided.draggableProps
: 태그의 모든 영역에서 드래그 가능하도록 설정provided.dragHandleProps
: 특정 영역에서만 드래그 가능하도록 설정<Droppable />
에서 가장 끝부분에 {provided.placeholder}
를 부여하여 가장 끝이 어딘지 알 수 있도록 설정한다.
import Item from "../components/Item";
import { InterfaceContent } from "../atoms";
import { Droppable } from "react-beautiful-dnd";
type ListProps = {
title: string;
contents: InterfaceContent[];
listId: string;
};
function List({ title, contents, listId }: ListProps) {
return (
<Droppable droppableId={listId}>
{(provided) => (
<div className="pt-2">
<p className="text-lg font-bold"> {title}</p>
<ul ref={provided.innerRef} {...provided.droppableProps}>
{contents.map((content, index) => (
<Item key={content.id} index={index} {...content} />
))}
</ul>
{provided.placeholder}
</div>
)}
</Droppable>
);
}
export default List;
위의 설정까지만 부여하고 나면 드래그앤드롭 애니메이션이 가능하나 마우스를 떼는 순간 다시 원래대로 돌아가버린다. 따라서 <DragDropContext />
의 onDragEnd
에 순서를 변경해주는 함수를 넣어줘야 한다.
DragDropContext
(자세한 설명은 여기를 참고)onBeforeCapture
: a drag is about to start and dimensions have not been collected from the DOM. 예제를 보니 드래그가 시작할때 쓰레기통이 생기는 요소를 이 함수로 사용한다.onDragStart
: 드래그가 시작할 때 호출onDragUpdate
: 드래그가 시작할 때 & 요소 간의 순서가 바뀔 때 호출따라서 onBeforeCapture
함수에게 쓰레기통이 보이도록 하는 소스코드를 추가해주면 된다.
개발팀의 github 링크 보고 구현했다.
const getStyle = (
style: DraggingStyle | NotDraggingStyle,
snapshot: DraggableStateSnapshot,
isOverTrashCan: boolean
) => {
if (!snapshot.isDropAnimating || !isOverTrashCan) {
return style;
}
return {
...style,
// cannot be 0, but make it super tiny
transitionDuration: `0.001s`,
};
};
<Draggable draggableId={item.id + ""} index={index}>
{(provided, snapshot) => (
<ItemCard
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
isOverTrashCan={isOverTrashCan}
isDragging={snapshot.isDragging}
style={getStyle(
provided.draggableProps.style!,
snapshot,
isOverTrashCan
)}
>
//...
드래그가 발생할 때마다 쓰레기통 위에 올라갔는지 체크해주면 된다. DragDropContext
의
onDragUpdate
메소드에서 체크해준다.
<DragDropContext
onDragEnd={dragEndHandler}
onBeforeCapture={() => setIsDragging(true)}
onDragUpdate={(update) => {
if (update.destination?.droppableId === "trashcan") {
setIsOverTrashCan(true);
} else {
setIsOverTrashCan(false);
}
}}
>
// ...
</DragDropContext>
그리고 드래그가 발생한 아이템만 색을 변경해줘야 하므로 Draggable
요소에서 두번째 인자로 전달되는 snapshot
의 isDragging
값을 가져온다.
<Draggable draggableId={item.id + ""} index={index}>
{(provided, snapshot) => (
<ItemCard
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
isOverTrashCan={isOverTrashCan}
isDragging={snapshot.isDragging}
>
// ...
</Draggable>
interface ICard {
isOverTrashCan: boolean;
isDragging: boolean;
}
const ItemCard = styled.li<ICard>`
// ...
box-shadow: ${(props) =>
props.isOverTrashCan && props.isDragging ? "0 0 0.3rem #d34747b4" : ""};
`;
실제 CSS가 변경될 때는 쓰레기통 위에 올라가있는지 & 현재 움직이고 있는 요소인지를 동시에 체크해서 값을 넘겨줘야 한다.