- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 자바스크립트
- react toast
- 리액트 알림
- localStorage
- sessionStorage
- angular
- 오블완
- next.js toast
- 리액트 라이프사이클
- 비동기 병렬처리
- The above error occurred in the
- 자바스크립트 순수함수
- 훈련소 후기
- useformstatus
- 산업기능요원 훈련소
- NextJS
- svgr
- 오라클클라우드
- resolved to branch.
- server action
- 리액트
- styled-component
- react life sycle
- 사회복무요원 훈련소
- no-use-before-define
- query param
- react
- 훈련소
- 공익 훈련소
- useRouter
아 그거 뭐였지
[React] react-beautiful-dnd 로 드래그 만들기 (TypeScript) 본문
Q. 드래그앤드랍 만들어주세요.
드래그앤드랍? 라이브러리 써서 하면 금방 뚝딱아닌가 라고 자만했던 과거의 나에게 엄벌을 내려주고싶다.
개발할때 자만은 죄악이다. 언제나 겸손하도록 하자.
앵귤러만 사용하다가 리액트로 사용하려다보니 막히는 부분이 많았다. 하지만 덕분에 공부가 많이돼서 재밌었다.
야무지게 다운로드 부터 먼저 해주도록 하자
# yarn
yarn add react-beautiful-dnd
# npm
npm install react-beautiful-dnd --save
공식문서를 보면 드래그 앤 드랍을 작동시키기 위해선 크게 DragDropContext와 Droppable, Draggable이 있는데
DragDropContext는 드래그 앤 드랍을 사용하기위한 영역을 지정해주는것이고
Droppable은 드랍한 요소들을 놓을수있는곳이다.
Draggble은 드랍 요소들을 뜻한다.
DragDropContext > Droppable > Draggble 이런식으로 DragDropContext가 가장 큰 범위라고 생각하였다.
설명만 봐서는 이해가 잘 안가서 코드를 보는게 이해가 더 빨랐다. 코드를 살펴보자
const droppableArr = ['before', 'after'];
const resultUserDatas:any = (dropId: string) => {
return userList?.userItems.map((user, idx) => {
if (user.dropId === dropId && user.name)
return (
<Draggable draggableId={user.id.toString()} index={idx} key={user.id}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
<span>{user.name}</span>
<span>{user.age}</span>
<span>{user.gender}</span>
</div>
)}
</Draggable>
);
});
};
return (
<>
<DragDropContext onDragEnd={(result) => onDragEnd(result)}>
{droppableArr.map((v) => {
return (
<>
<Droppable droppableId={v} key={v}>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
key={v}
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
className={v}
>
{resultUserDatas(v)}
{provided.placeholder}
</div>
)}
</Droppable>
<div style={{ backgroundColor: 'red', width: 100 }}></div>
</>
);
})}
</DragDropContext>
</>
);
코드에보면 DragDropContext가 감싸고 안에 Droppable영역과 Droppable 안에 드래그할 요소인 Draggble이 있다.
본인은 드랍할 영역을 여러개를 사용하고싶었기때문에 droppableArr이라는 배열을 선언하였고 droppableArr의 갯수만큼 droppable을 만든다. 해당 droppalbeId는 arr의 요소가된다. 이때 값이 겹치면 안된다.
또한 draggble 요소를 여러개 사용할예정이여서 resultUserDatas라는 함수로 따로 뺐다.
※중요※
- droppableId값과 key값이 같아야 정상작동한다. 또한 droppableId의값을통해서 드래그한곳이 어디인지 판별한다. 따라서 값이 유니크해야만하며 겹쳐서는 안된다.
- DragDropContext에는 onDragEnd를 지정해주어야한다. (드래그앤드랍 이벤트가 발생했을때 어떤 동작을 할건지 처리하는부분)
- droppalbe의 요소에서 끝나는 부분에 provided.placeholder를 써줘야한다. 어떤 역할을 하는지 공식문서에서 찾아보았는데 드래그를하기위해 공간을 만드는 요소로 보인다.
provided.placeholder : 드래그하는 동안 필요에 따라 공간을 만드는 데 사용됩니다. 이 공간은 사용자가 홈 목록이 아닌 목록 위로 끌 때 필요합니다.
provided는 드래그앤드랍시 필요한 데이터들이 담겨있다. snapshot은 드래그할때의 상태들을 담고있다.
ref={provided.innerRef}는 꼭 최상단 element에다가 넣어주어야한다.
만약 구조가 아래와같다면 가장 바깥에있는 div태그에다가 <div ref={provided.innerRef}> 이렇게 해주어야한다.
<div ref={provided.innerRef}>
<ul>
<li></li>
</ul
</div
droppableId의 값을통해 어디로 드래그 했는지를 판별한다고했는데
if (user.dropId === dropId && user.name)
이 if문을 통해서 해당되는 영역에서만 렌더링되게 하는것이다.
ex) A영역에서의 droppableId가 dropA고 B영역에서의 droppableId가 dropB인 상황에서 A에있는 드래그 요소를 B로 옮겼다면 droppableId가 B인 드래그 요소만을 보여줘야한다.
onDragEnd함수는 아래와같이 작성하였다.
const onDragEnd = (result: any) => {
console.log(result);
// dropped outside the list(리스트 밖으로 드랍한 경우)
if (!result.destination) {
return;
}
const { source, destination } = result;
let items = [...userList.userItems];
let index;
//droppableId가 다르면 영역이 다르다는 얘기
//같으면 같은 영역에서 순서만바꾸거나 그냥 집었다가 놓은경우
if (source.droppableId !== destination.droppableId) {
index = items.findIndex((v) => v.id === parseInt(result.draggableId));
let findObj = items[index];
findObj.dropId = destination.droppableId;
items.splice(index, 1);
items = [...items, findObj];
setUserList({
userItems: items,
});
} else {
if (source.index !== destination.index) {
let selectItem = items[result.source.index];
items.splice(result.source.index, 1);
items.splice(destination.index, 0, selectItem);
setUserList({
userItems: items,
});
}
}
};
result를 파라미터로 받고있는데 result는 DragDropContext의 메소드인 onDragEnd의 결과값이다.
console.log로 값을 찍어보면 대략
현재위치의(source) id값과 index값,
드랍하려는 위치의(destination) id값과 index값이 나온다.
이것으로 id값을 비교해서 다르면 다른 영역으로 드래그앤드랍이 행해진것이고 같다면 현재 영역에서만 드래그앤드랍이 이루어진것으로 판별할수있다.
id값이 다르다면 해당 드래그 요소의 index를 찾아서 droppableId를 드랍하려는 위치의 id값으로 바꿔주고 원본 배열을 바꿔준다.
id값이 같다면 현재영역에서의 드래그앤드랍이 행해졌다는것인데, 여기서 index값이 같다면 순서가 바뀐게없으므로 아무행위도 하지않으면된다. index값이 다르다면 순서가 바뀌었다는 것이므로 해당하는 드래그요소를 원본배열에서 삭제한뒤
드래그앤드랍으로 원하는 위치에 넣었던 index값을 가지고 배열에 추가해주면된다.
전체 소스는 아래에 첨부한다.
나이,이름,성별을 가진 object를 생성하고(createUser) 생성한 object들로 드래그앤드랍하는 예제이다.
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import DragItem from './DragItem';
interface ISlotItem {
id: number;
age: number;
name: string;
gender: string;
dropId: string;
}
interface ISlotList {
userItems: ISlotItem[];
}
const Input = styled.input`
padding: 10px;
width: 200px;
height: 40px;
`;
const AgeInput = styled.input.attrs({
type: 'number',
})`
padding: 10px;
width: 200px;
heigth: 40px;
`;
const GenderButton = styled.button`
padding: 10px;
width: 100px;
height: 30px;
`;
const SubmitButton = styled.button`
width: 100px;
heigth: 50px;
padding: 10px;
text-align: center;
`;
function DragMain() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [gender, setGender] = useState('');
const arr = ['before', 'after'];
const [userItem, setUserItem] = useState<ISlotItem>({
id: 0,
name: '',
age: 0,
gender: '',
dropId: arr[0],
});
const [userList, setUserList] = useState<ISlotList>({
userItems: [userItem],
});
const getItemStyle = (isDragging: any, draggableStyle: any) => ({
userSelect: 'none',
padding: 10,
margin: `5 0`,
border: '1px solid #ccc',
background: isDragging ? 'lightgreen' : 'red',
...draggableStyle,
});
const onUserList = useCallback(() => {
setUserList({
userItems: [...userList.userItems, userItem],
});
}, [userItem]);
useEffect(() => {
onUserList();
}, [onUserList]);
let [idCnt, setIdCnt] = useState(0);
const createUser = (name: string, age: number, gender: string) => {
if (!name || age < 1 || !gender) {
alert('사용자 정보 입력필요');
return;
}
setIdCnt((idCnt += 1));
setUserItem({ id: idCnt, name, age, gender, dropId: 'before' });
};
const resultUserDatas: any = (dropId: string) => {
return userList?.userItems.map((user, idx) => {
if (user.dropId === dropId && user.name)
return (
<Draggable draggableId={user.id.toString()} index={idx} key={user.id}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
<span>{user.name}</span>
<span>{user.age}</span>
<span>{user.gender}</span>
</div>
)}
</Draggable>
);
});
};
const onDragEnd = (result: any) => {
console.log(result);
// dropped outside the list(리스트 밖으로 드랍한 경우)
if (!result.destination) {
return;
}
const { source, destination } = result;
let items = [...userList.userItems];
let index;
if (source.droppableId !== destination.droppableId) {
index = items.findIndex((v) => v.id === parseInt(result.draggableId));
let findObj = items[index];
findObj.dropId = destination.droppableId;
items.splice(index, 1);
items = [...items, findObj];
setUserList({
userItems: items,
});
} else {
if (source.index !== destination.index) {
let selectItem = items[result.source.index];
items.splice(result.source.index, 1);
items.splice(destination.index, 0, selectItem);
setUserList({
userItems: items,
});
}
}
};
const getListStyle = (isDraggingOver: any) => ({
background: isDraggingOver ? 'lightblue' : 'grey',
padding: 10,
width: 250,
});
return (
<>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Input onChange={(e) => setName(e.target.value)} />
<AgeInput onChange={(e) => setAge(parseInt(e.target.value))} />
<GenderButton onClick={() => setGender('남자')}>남자</GenderButton>
<GenderButton onClick={() => setGender('여자')}>여자</GenderButton>
<SubmitButton onClick={() => createUser(name, age, gender)}>
사용자 등록
</SubmitButton>
</div>
<DragDropContext onDragEnd={(result) => onDragEnd(result)}>
{arr.map((v) => {
return (
<>
<Droppable droppableId={v} key={v}>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
key={v}
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
className={v}
>
{resultUserDatas(v)}
{provided.placeholder}
</div>
)}
</Droppable>
<div style={{ backgroundColor: 'red', width: 100 }}></div>
</>
);
})}
</DragDropContext>
</div>
</>
);
}
export default DragMain;
사용법을 크게 세줄요약하자면 아래와같다.
- DragDropContext 안에 Droppable을 정의하고 드래그할 요소들을 Drappbale안에 Draggable로 넣는다.
- DragDropContext에는 onDragEnd가 꼭 필요하다.
- DroppableId는 유니크해야한다. (타입스크립트로 작성하니 DroppableId는 꼭 string형태가 들어가야하는것같다.)
- 공식문서 샘플예제를 한번 봐보자
참고링크 : GitHub - atlassian/react-beautiful-dnd: Beautiful and accessible drag and drop for lists with React
'Front-End' 카테고리의 다른 글
[Jquery] document.getElementsByClassName을 제이쿼리로 (1) | 2022.09.14 |
---|---|
[React] useCallBack, Memoization, 불필요한 함수 초기화 막기 (0) | 2022.09.13 |
[Angular] ngFor 배열선언없이 동적값으로 반복 출력하기 (0) | 2022.08.25 |
[CSS] 폰트 직접 추가해서 사용하기 (0) | 2022.08.22 |
[React] 리액트 useNavigate 페이지 이동 (0) | 2022.07.28 |