[react-sortablejs] 그리드 형태 드래그앤드롭 구현 라이브러리
그리드 형태에 개별 요소의 위치를 바꿀수도 있는 기능이 필요했다.
보통은 react-beautiful-dnd를 많이 쓰는것 같은데 내 상황에 맞는 옵션을 잘 모르겠어서(열을 넘나드는 형태는 있는데 나한테 딱 맞는 상황이라고 하기에는 애매했다.)
그리드 예제가 있었던 react-sortablejs 를 사용했다. 리액트 버전은 공식사이트가 없고 깃허브뿐인데 비슷하게 그리드 예제가 있고 공식사이트가 있는 react-sortable-hoc 를 사용해도 좋을 듯. 시각적인 예제들도 있고 사실 저기가 더 다운로드수 많음
설치하기
npm install --save react-sortablejs sortablejs
// 타입스크립트 사용 시
npm install --save-dev @types/sortablejs
# OR
yarn add react-sortablejs sortablejs
// 타입스크립트 사용 시
yarn add -D @types/sortablejs
공식 문서 기본 세팅
import { ReactSortable } from "react-sortablejs";
import { useState } from "react";
export const BasicFunction = (props) => {
const [state, setState] = useState([
{ id: 1, name: "shrek" },
{ id: 2, name: "fiona" },
]);
return (
<ReactSortable list={state} setList={setState}>
{state.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</ReactSortable>
);
};
상태를 기반으로 동작한다. 비동기적으로 데이터를 받아온 다음 setState 후 진행해도 좋음
내가 사용한 방법
const MainSub = () => {
const [list, setList] = useState([
{
title: "lorem ipsum",
children: "test",
},
{
title: "이",
children: "2",
},
{
title: "삼",
children: "3",
},
{
title: "사",
children: "4",
},
{
title: "오",
children: "5",
},
{
title: "육",
children: "6",
},
]);
const onDragDropEnds = (oldIndex, newIndex) => {
console.log(oldIndex, newIndex);
}; // 드래그가 끝났을 때 실행할 콜백, oldIndex는 요소가 처음 위치했던 인덱스, newIndex는 새로 들어간 인덱스
return (
<Wrapper>
<ReactSortable
list={list}
setList={(newList) => setList(newList)}
handle=".dragHandle" // 드래그를 촉발시킬 className 지정
animation="300" // 요소끼리 자리가 바뀔 때 부드럽게 이동
className="grid-container" // css 스타일링 하고싶으면 지정
onEnd={({ oldIndex, newIndex }) => onDragDropEnds(oldIndex, newIndex)}
/* 드래그가 끝났을 때 실행할 함수 */
>
{list.map((li, i) => (
<MenuBox key={i} title={li.title} children={li.children} draggable />
/* 이 컴포넌트의 draggable은 컴포넌트 커스텀용으로 만든 속성,
드래그 이벤트나 sortablejs와 관계 x */
))}
</ReactSortable>
</Wrapper>
);
};
여기서 map에서 반환하는 컴포넌트는 빠르게 만들려고 키값을 인덱스로 줬는데 실제로 사용할때는 이러지 마세용 ^,,^
const MenuBox = ({ children, title, draggable, ...props }, ref) => {
const displayTitle = title || "테스트";
return (
<Box {...props}>
<div className="box__title">
<Title bg="#fff">{displayTitle}</Title>
<div className="button__box">
<Button bg={primary.main} color="#fff" fz={"14px"}>
+More
</Button>
{draggable && <DragHandleIcon className="drag-icon dragHandle" />}
/* ✨dragHandler 클래스 추가✨ */
</div>
</div>
<div className="box__content">
{children ? children : `No ${displayTitle} contents.`}
</div>
</Box>
);
};
컴포넌트에서 handle=".dragHandle"
형태로 prop을 줬었다. handle에 넣은 클래스명을 지정해주면 그 요소를 잡고 드래그를 할 수 있다. 나는 컴포넌트의 모든 부분이 아니라 오른쪽 상단의 아이콘을 통해서만 움직일 수 있게 하고 싶었기 때문에 아이콘에 dragHandle 클래스를 주었다.
HTML5에서는 기본적으로 드래그중인 요소를 클론하고 그 클론한 요소의 opacity를 반투명하게 낮춰버리는 특성이 있다.
하지만 나는 클론된 요소를 진하게 만들고 아래에 남아있는 원본 요소를 보이지 않게 만들고 싶었다. 겹쳐 보이니 헷갈리기도 하고 움직이고 있다는것을 더 명확하게 하고 싶었다.
<Wrapper>
<ReactSortable
list={list}
setList={(newList) => setList(newList)}
handle=".dragHandle"
animation="300"
forceFallback={true} // ✨추가하기✨
className="grid-container"
onEnd={({ oldIndex, newIndex }) => onDragDropEnds(oldIndex, newIndex)}
>
{list.map((li, i) => (
<MenuBox title={li.title} children={li.children} draggable />
))}
</ReactSortable>
</Wrapper>
forceFallback
은 HTML이 기본적으로 하는 행동들 (위에서 말한 opacity 낮추기) 을 없애고 이를 강제로 변경할 수 있도록 해준다. fallback은 대체라는 뜻을 가지고 있는데 원래 제공되던 반투명한 요소 대신 대체 요소를 채워 넣어서 마음대로 커스텀 할 수 있게 되는 것이다.
클론되어 딸려올라오는 요소는 .sortable-fallback
이라는 클래스명을 가지고 있다. 이 클래스명을 통해 스타일링을 마음대로 할 수 있다.
또한 현재 드래그중인 원본 요소는 .sortable-chosen
이라는 클래스명을 가진다. 이것을 통해 아래에 있는 요소를 지워주었다.
.sortable-fallback {
opacity: 1 !important;
/* 원하는 스타일링 하기 */
}
.sortable-chosen {
opacity: 0;
}
색이 잘 안보여서 배경색을 변경해 주었다. 드래그중인 대상을 인지하는 게 훨씬 쉬워졌다.
아쉬운 점?
윈도우에서는 드래그를 늘였다가 놓았을때 천천히 제자리로 돌아가는 애니메이션이 되지 않는 것 같다.
react-beautiful-dnd 에서는 된다. 이건 원하지 않는 사람도 있으니 필요한대로 선택하면 될듯