Drag and Drop 기능 추가하기
npm install react-beautiful-dnd --save
<DragDropContext /> - Wraps the part of your application you want to have drag and drop enabled for
<Droppable /> - An area that can be dropped into. Contains <Draggable /> s
<Draggable /> - What can be dragged around
이를 List.js에 import 해줍니다.
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
<List.js>
import React from 'react';
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
export default function List({ todoData, setTodoData }) {
const handleClick = (id) => {
let newTodoData = todoData.filter(data => data.id !== id);
console.log('newTodoData', newTodoData);
setTodoData(newTodoData);
};
const handleCompleteChange = (id) => {
let newTodoData = todoData.map(data => {
if(data.id === id) {
data.completed = !data.completed;
}
return data;
});
setTodoData(newTodoData);
};
return (
<div>
<DragDropContext>
<Droppable droppableId="todo-list">{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{todoData.map((data, index) => (
<Draggable
key={data.id}
draggableId={data.id.toString()}
index={index}
>
{(provided, snapshot) => (
<div
key={data.id}
{...provided.draggableProps}
ref={provided.innerRef}
{...provided.dragHandleProps}
className={`${
snapshot.isDragging ? "bg-gray-400": "bg-gray-100"
} flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 border rounded`}
>
<div className='items-center'>
<input
type="checkbox"
defaultChecked={data.completed}
onChange={() => handleCompleteChange(data.id)}
/>
{" "}
<span className={data.completed && "line-through"}>{data.title}</span>
</div>
<div>
<button
className="px-4 py-2 float-right"
onClick={() => handleClick(data.id)}
>
x
</button>
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
);
}
provided object에는 스타일 지정 및 조회를 위한 속성이 포함되어 있다.
사용자가 요소를 드래그하는 경우 className 속성을 selected 로 변경한다.
나중에 스타일을 적용하는 데 사용할 것이다.
placeholder 속성은 목록에 빈 공간을 만든다. 이렇게 하면 드래그 작업이 자연스럽게 느껴질 것이다.
일단 여기까지 코드가 작성되면, drag가 되고, drag하는 항목에는 클릭 되어있는 동안 진한 회색으로 색상이 변경되어 나타날 것이다. 하지만 아직 drop을 통한 순서 적용이 되지 않는다.
splice() 메서드는 배열의 기존 요소를 삭제 또는 교체하거나 새 요소를 추가하여 배열의 내용을 변경합니다.
예를 들어 다음 코드를 살펴보면,
const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');
// Inserts at index 1
console.log(months);
// Expected output: Array ["Jan", "Feb", "March", "April", "June"]
months.splice(4, 1, 'May');
// Replaces 1 element at index 4
console.log(months);
// Expected output: Array ["Jan", "Feb", "March", "April", "May"]
실행결과
> Array ["Jan", "Feb", "March", "April", "June"]
> Array ["Jan", "Feb", "March", "April", "May"]
다음과 같이 작동하는 메서드이다.
이 splice 함수를 dragging 한 후 drop을 할 때 데이터 순서를 적용시키기 위해 사용해보면,
**<List.js>**
import React from 'react';
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
export default function List({ todoData, setTodoData }) {
const handleClick = (id) => {
let newTodoData = todoData.filter(data => data.id !== id);
console.log('newTodoData', newTodoData);
setTodoData(newTodoData);
};
const handleCompleteChange = (id) => {
let newTodoData = todoData.map(data => {
if(data.id === id) {
data.completed = !data.completed;
}
return data;
});
setTodoData(newTodoData);
};
const handleEnd = (result) => {
//result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함됩니다.
console.log(result)
//목적지가 없으면(이벤트 취소) 이 함수를 종료합니다.
if (!result.destination) return;
// 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
const newTodoData = todoData;
//1. 변경시키는 아이템을 배열에서 지워준다.
//2. return 값으로 지워진 아이템을 잡아준다.
const [reorderedItem] = newTodoData.splice(result.source.index, 1);
//원하는 자리에 redorderItem을 insert 해준다.
newTodoData.splice(result.destination.index, 0, reorderedItem);
setTodoData(newTodoData);
}
return (
<div>
<DragDropContext onDragEnd={handleEnd}>
<Droppable droppableId="todo">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{todoData.map((data, index) => (
<Draggable
key={data.id}
draggableId={data.id.toString()}
index={index}
>
{(provided, snapshot) => (
<div
key={data.id}
{...provided.draggableProps}
ref={provided.innerRef}
{...provided.dragHandleProps}
className={`${
snapshot.isDragging ? "bg-gray-400": "bg-gray-100"
} flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 border rounded`}
>
<div className='items-center'>
<input
type="checkbox"
defaultChecked={data.completed}
onChange={() => handleCompleteChange(data.id)}
/>
{" "}
<span className={data.completed && "line-through"}>{data.title}</span>
</div>
<div>
<button
className="px-4 py-2 float-right"
onClick={() => handleClick(data.id)}
>
x
</button>
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
);
}
이를 실행시키면
drag and drop이 잘 작동되는 것을 볼 수 있다.
리액트 불변성 지키기
불변성이란 사전적 의미로는 값이나 상태를 변경할 수 없는 것을 의미한다.
자세히 알아보기 위해 자바스크립트 타입을 통해서 알아보자.
원시 타입은 불변성(immutable)을 가지고 있고,
참조 타입은 그렇지 않기 때문에(mutable)
둘을 비교하며 불변성의 의미를 더 자세히 알아보자.
기본적으로 Javascript는 원시 타입에 대한 참조 및 값을 저장하기 위해 Call Stak 메모리 공간을 사용하지만,
참조 타입의 경우 Heap이라는 별도의 메모리 공간을 사용한다.
이 경우 Call Stack은 개체 및 배열 값이 아닌 메모리에만 Heap 메모리 참조 ID를 값으로 저장한다.
참조 타입에서 객체나 배열의 값이 변할 때 원본 데이터가 변경되기에 이 원본 데이터를 참조하고 있는 다른 객체에서 예상치 못한 오류가 발생할 수 있어서 프로그래밍의 복잡도가 올라간다.
리액트에서 화면을 업데이트할 때 불변성을 지켜서 값을 이전 값과 비교해서 변경된 사항을 확인한 후 업데이트하기 때문에 불변성을 지켜줘야 한다.
참조 타입에서는 값을 바꿨을 때 Call Stack 주소 값은 같은데 Heap 메모리 값만 바꿔주기에 불변성을 유지할 수 없었으므로 아예 새로운 배열을 반환하는 메소드를 사용하면 된다.
원본 데이터를 변경하지 않는 메소드(불변성을 지킬 때 사용 => spread operator('...'), map, filter, slice, reduce
원본 데이터를 변경하는 메소드 => splice, push
List 컴포넌트 생성하기
rfc+Enter키를 입력하면 다음과 같이 생성된다.
import React from 'react'
export default function List() {
return (
<div>List</div>
)
}
Lists.js에 있던 UI 부분의 코드를 옮긴다. 옮길 코드는 아래와 같다.
<div
key={data.id}
{...provided.draggableProps}
ref={provided.innerRef}
{...provided.dragHandleProps}
className={`${
snapshot.isDragging ? "bg-gray-400": "bg-gray-100"
} flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 border rounded`}
>
<div className='items-center'>
<input
type="checkbox"
defaultChecked={data.completed}
onChange={() => handleCompleteChange(data.id)}
/>
{" "}
<span className={data.completed && "line-through"}>{data.title}</span>
</div>
<div>
<button
className="px-4 py-2 float-right"
onClick={() => handleClick(data.id)}
>
x
</button>
</div>
</div>
Lists.js에 있던 handleCompleteChange, handleClick 함수도 옮긴다. 옮길 코드는 아래와 같다.
const handleClick = (id) => {
let newTodoData = todoData.filter(data => data.id !== id);
console.log('newTodoData', newTodoData);
setTodoData(newTodoData);
};
const handleCompleteChange = (id) => {
let newTodoData = todoData.map(data => {
if(data.id === id) {
data.completed = !data.completed;
}
return data;
});
setTodoData(newTodoData);
};
import를 다음과 같이 해준다.
import List from './List';
그리고 Lists.js에서 props로 내려준다.
<Lists.js>
{(provided, snapshot) => (
<List
keys={data.id}
id={data.id}
title={data.title}
completed={data.completed}
todoDate={todoData}
setTodoData={setTodoData}
provided={provided}
snapshot={snapshot}
/>
그리고 List.js에서 이를 받아온다. 다음과 같이 변경해준다.
<변경 전 List.js>
export default function List() {
<변경 후 List.js>
const List = ({
id,
title,
completed,
todoData,
setTodoData,
provided,
snapshot
}) => {
...
export default List;
그리고 마지막으로 List.js에서 필요하지 않은 'data.' 부분을 지워준다.
따라서 Lists.js와 List.js코드는 다음과 같이 된다.
<Lists.js>
import React from 'react';
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import List from './List';
export default function Lists({ todoData, setTodoData }) {
const handleEnd = (result) => {
//result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함됩니다.
console.log(result)
//목적지가 없으면(이벤트 취소) 이 함수를 종료합니다.
if (!result.destination) return;
// 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
const newTodoData = todoData;
//1. 변경시키는 아이템을 배열에서 지워준다.
//2. return 값으로 지워진 아이템을 잡아준다.
const [reorderedItem] = newTodoData.splice(result.source.index, 1);
//원하는 자리에 redorderItem을 insert 해준다.
newTodoData.splice(result.destination.index, 0, reorderedItem);
setTodoData(newTodoData);
}
return (
<div>
<DragDropContext onDragEnd={handleEnd}>
<Droppable droppableId="todo">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{todoData.map((data, index) => (
<Draggable
key={data.id}
draggableId={data.id.toString()}
index={index}
>
{(provided, snapshot) => (
<List
keys={data.id}
id={data.id}
title={data.title}
completed={data.completed}
todoDate={todoData}
setTodoData={setTodoData}
provided={provided}
snapshot={snapshot}
/>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
);
}
<List.js>
import React from 'react'
const List = ({
id,
title,
completed,
todoData,
setTodoData,
provided,
snapshot
}) => {
const handleClick = (id) => {
let newTodoData = todoData.filter(data => data.id !== id);
console.log('newTodoData', newTodoData);
setTodoData(newTodoData);
};
const handleCompleteChange = (id) => {
let newTodoData = todoData.map(data => {
if(data.id === id) {
completed = !completed;
}
return data;
});
setTodoData(newTodoData);
};
return (
<div
key={id}
{...provided.draggableProps}
ref={provided.innerRef}
{...provided.dragHandleProps}
className={`${
snapshot.isDragging ? "bg-gray-400": "bg-gray-100"
} flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 border rounded`}
>
<div className='items-center'>
<input
type="checkbox"
defaultChecked={completed}
onChange={() => handleCompleteChange(id)}
/>
{" "}
<span className={completed && "line-through"}>{title}</span>
</div>
<div>
<button
className="px-4 py-2 float-right"
onClick={() => handleClick(id)}
>
x
</button>
</div>
</div>
)
}
export default List;