구현해야 하는 기능 중 드래그 & 드롭 기능이 필요해졌다.
관련 라이브러리를 여러가지 찾다보니 React-dnd에
애니메이션이 첨가된 React-beautiful-dnd라이브러리를 사용하게 됐다.
그래서 오늘은 간단하게 기능을 구현해보고 라이브러리를 체득해보자
설치
먼저 프로젝트에 설치부터 해보자.
npm install react-beautiful-dnd
참고로 프로젝트는 React Next.js를 사용해서 구현해볼 것이다.
react-beautiful-dnd를 사용하려면 두가지 설정을 필수로 해야한다.
- strictMode끄기
- requestAnimationFrame실행 후 랜더링하기
프로젝트 루트에 next.config.js에서 strictMode를 아래와 같이 꺼주면 되며
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
}
module.exports = nextConfig
구현하기 전에 dnd가 적용 될 컴포넌트에 작성되어야 할 requestAnimationFrame 셋팅
import { useEffect, useState } from "react";
import { styled } from "styled-components";
export default function App(props) {
// requestAnimationFrame 실행여부
const [enabled, setEnabled] = useState(false);
useEffect(() => {
// 컴포넌트 랜더링 하기 전 requestAnimationFrame 실행
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
// requestAnimationFrame 정지
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
// requestAnimationFrame 실행 이후 랜더링
if (enabled)
return (
<div>
드래그앤드롭
</div>
);
}
다음으로는 드래그&드롭 할 데이터가 필요하기때문에 다음과 같이 임시데이터를 적용해준다.
const [data,setData] = useState([]);
// useEffect 내에서 초기 데이터 생성
setData([
{id:'id_1',name:'data1'},
{id:'id_2',name:'data2'},
{id:'id_3',name:'data3'},
{id:'id_4',name:'data4'},
{id:'id_5',name:'data5'},
{id:'id_6',name:'data6'},
{id:'id_7',name:'data7'},
{id:'id_8',name:'data8'},
])
구현
각 드래그 아이템을 구분하기 좋게 하기위해 Styled-components를 추가해 Box와 Item에 스타일을 주었다.
import {
DragDropContext,
Draggable,
Droppable
} from "react-beautiful-dnd";
... Component codes
// 아이템을 드래그 했을때 list의 순서를 바꿔주는 함수
const handleDragEnd = ({ source, destination }) => {
// 드래그가 취소되거나 드랍 위치가 없을 때
if (!destination) {
return;
}
const newData = Array.from(data);
const [movedItem] = newData.splice(source.index, 1);
newData.splice(destination.index, 0, movedItem);
setData(newData);
};
return (
<div>
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="droppable">
{(dropProvided) => (
<Box ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
{data.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(dragProvided) => (
<Item
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
{item.name}
</Item>
)}
</Draggable>
))}
{dropProvided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
</div>
);
... Component codes
const Box = styled.div`
background: skyblue;
padding: 30px;
display:flex;
flex-direction:column;
align-items:center;
gap: 8px;
`;
const Item = styled.p`
width:100%;
height:100%;
border: 1px solid red;
font-size: 24px;
font-weight: 600;
`;
이제 하나하나 뜯어보자면
- DragDropContext를 사용해서 Drag&Drop을 사용 할 구역을 정해준다.
- Droppable을 사용해 아이템을 드래그 후 떨어뜨릴 수 있는 구역을 지정한다.
- Draggable을 사용해 드래그 할 수 있는 아이템을 지정해 준다.
자세히 보면 Droppable과 Draggable 내부에 provided 객체를 가져와 설정값들을 입력해주는데
위 코드처럼 dropProvided와 dragProvided는 엄연히 다른객체이기 때문에 헷갈리거나 이름을 같게해 가독성을 떨어뜨리지 않는 편이 좋을 것 같다.
Droppable
<Droppable droppableId="droppable">
{(dropProvided) => (
<Box ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
{data.map((item, index) => (
...dragable
))}
{dropProvided.placeholder}
</Box>
)}
</Droppable>
* droppableId
- 드롭 가능한 영역을 구분할 id를 표시한다. ex) todo, doing, done
- ID가 유닉하지만 재활용된다면 버그를 일으킬 수 있다.
* dropProvided.innerRef
- 라이브러리에서 우리 컴포넌트 DOM을 조작하기 위해서 필수로 등록해줘야 한다.
* dropProvided.droppableProps
- It currently contains data attributes that we use for styling and lookups.
- 그냥 우리가 전달한 props를 라이브러리에서 사용할 수 있는 형태로 DOM data에 등록시켜주는 것 같다.
* dropProvided.placeholder
- This is used to create space in the <Droppable /> as needed during a drag.
- drop될 때 공간을 만들기 위해서 필요하다고 한다.
Draggable
<Draggable key={item.id} draggableId={item.id} index={index}>
{(dragProvided) => (
<Item
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
{item.name}
</Item>
)}
</Draggable>
* draggableId
- 드롭 가능한 영역을 구분할 id를 표시한다. ex) todo, doing, done
- ID가 유닉하지만 재활용된다면 버그를 일으킬 수 있다.
* index
- 리스트의 순서대로 입력해야 한다.
* dragProvided.innerRef
- 라이브러리에서 우리 컴포넌트 DOM을 조작하기 위해서 필수로 등록해줘야 한다.
* dragProvided.draggableProps
- contains a data attribute and an inline style.
- drag 스타일을 등록해주는 역할이다. 이게 없다면 엘리먼트가 움직이지 않을 것이다.
* dragProvided.dragHandleProps
- drag handle를 등록해주는 인자인데 살펴보면 내부로직이 어떻게 구현했는지 조금 힌트를 얻을 수 있다.
- data-rbd-drag-handle-draggable-id
- data-rbd-drag-handle-context-id
- aria-labelledby
- screen reader가 연관된 엘리먼트를 읽을 수 있도록 해준다.
- tabIndex
- 키보드 탭으로 엘리먼트를 접근할 수 있게 해준다.
- draggable
- onDragStart
- onDragStart를 통해서 이벤트를 등록해준다.
구현결과 테스트
정상적으로 잘 동작하는 것을 볼 수 있다.
실제로 구현을 할때 수정해야하는 것은 스타일, drag&drop으로 인해 실행될 handleDragEnd함수내 코드만 수정해주면
데이터가 다르더라도 정상동작할 것 같다.
전체코드
이번 연습과 함께 구현해본 전체 코드
import { useEffect, useState } from "react";
import {
DragDropContext,
Draggable,
Droppable
} from "react-beautiful-dnd";
import { styled } from "styled-components";
export default function App(props) {
// 아이템이 될 data list
const [data,setData] = useState([]);
// requestAnimationFrame 실행여부
const [enabled, setEnabled] = useState(false);
useEffect(() => {
// 초기 데이터 생성
setData([
{id:'id_1',name:'data1'},
{id:'id_2',name:'data2'},
{id:'id_3',name:'data3'},
{id:'id_4',name:'data4'},
{id:'id_5',name:'data5'},
{id:'id_6',name:'data6'},
{id:'id_7',name:'data7'},
{id:'id_8',name:'data8'},
])
// 컴포넌트 랜더링 하기 전 requestAnimationFrame 실행
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
// requestAnimationFrame 정지
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
// 아이템을 드래그 했을때 list의 순서를 바꿔주는 함수
const handleDragEnd = ({ source, destination }) => {
// 드래그가 취소되거나 드랍 위치가 없을 때
if (!destination) {
return;
}
const newData = Array.from(data);
const [movedItem] = newData.splice(source.index, 1);
newData.splice(destination.index, 0, movedItem);
setData(newData);
};
// requestAnimationFrame 실행 이후 랜더링
if (enabled)
return (
<div>
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="droppable">
{(dropProvided) => (
<Box ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
{data.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(dragProvided) => (
<Item
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
{item.name}
</Item>
)}
</Draggable>
))}
{dropProvided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
</div>
);
}
const Box = styled.div`
background: skyblue;
padding: 30px;
display:flex;
flex-direction:column;
align-items:center;
gap: 8px;
`;
const Item = styled.p`
width:100%;
height:100%;
border: 1px solid red;
font-size: 24px;
font-weight: 600;
`;
'Next.Js > Learn' 카테고리의 다른 글
React,Next.JS에서 Table Element를 Excel파일로 다운로드하기 (feat. XLSX) (0) | 2023.08.21 |
---|---|
Library React Swiper 응용하기 (module, props...) (0) | 2023.03.29 |
Library React Swiper 슬라이드 가능한 컴포넌트 만들기 (0) | 2023.03.27 |