March 13, 2020
๋๋๊ทธ ์ค ๋๋ ๊ธฐ๋ฅ์ ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ดํ๋ฆฌ์ผ์ด์ .
react-beautiful-dnd ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ณ , ์ด๋ฒค์ ธ์ค ์บ๋ฆญํฐ๋ค์ ์ปจํ ์ธ ๋ก ์ฌ์ฉํด๋ณด์๋ค.
ํ๋ฉด ๊ตฌ์ฑ์ ๋๋๊ทธ์ค๋๋ ๊ธฐ๋ฅ์ด ๋ค์ด์๋ ์๋จ ์ปจํ ์ด๋์ ๋ฐ์ดํฐ๋ฅผ ํ๋ฉด์ ๊ทธ๋ ค์ฃผ๋ ํ๋จ ์ปจํ ์ด๋, ํฌ๊ฒ ๋ ๊ฐ๋ก ๊ตฌ์ฑํ์๋ค.
<BackgroundContainer>
<ContentsMenubar name="avengers" data={dndData} />
<Description>Try Drag & Drop</Description>
<TopContainer>
<DragDropContext>
<DndContainer>{/* ๋๋๊ทธ์ค ๋๋ ๊ด๋ จ code */}</DndContainer>
</DragDropContext>
</TopContainer>
<Description>Make them friends</Description>
<div className="mobile_description">scroll >>> </div>
<BottomContainer>{/* ๋ฐ์ดํฐ -> ๋ทฐ code */}</BottomContainer>
</BackgroundContainer>
ContentsMenubar : ๋ฆฌ๋์์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฆฌ์ ์ํฌ ์ ์๋ ๋ฉ๋ด๋ฐ
Description : ์ค๋ช (styled-component)
TopContainer : ๋๋๊ทธ์ค ๋๋ ๊ธฐ๋ฅ, react-beautiful-dnd ์
DragDropContext API
๋ฅผ ์ฌ์ฉํ๋ค.
div.mobile_description : width๊ฐ BottomContainer ๋ณด๋ค ์์์ง๋ฉด ์คํฌ๋กคํ๋ผ๋ ์ค๋ช ๋ ๋ณด์ด๋๋ก ํด์ฃผ์๋ค.
BottomContainer : ๋ฐ์ดํฐ๋ฅผ ํ๋ฉด์ ๊ทธ๋ ค์ฃผ๋ ์ญํ
const data = useSelector(state => state.avengers)
const [dndData, setDndData] = useState(data)
๋ฆฌ์กํธ hooks๋ฅผ ์ด์ฉํด์ ์ปดํฌ๋ํธ ๋ด์์์ ์ํ๊ด๋ฆฌ๋ฅผ ํ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋๋๊ทธ์ค ๋๋์ผ๋ก ๋ฐ์ดํฐ์ ๋ณ๊ฒฝ์, ๋ถ๋ณ์ฑ์ ์ ์ง์์ผ์ฃผ๋ฉฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊พธ์ด์ฃผ์๋ค.
dndData : ๊ฐ์ฒดํํ์ ํ์ฌ column, item๋ค์ ๋ฐ์ดํฐ
๋ฆฌ๋์ค hooks๋ฅผ ์ด์ฉํ๋ค.
contentsMenuBar์์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฒํผ์ ๋๋ฅด๋ฉด useDispatch
๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ
useSelector
๋ก ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์๋ค.
// reducers > avengers.js
const initialState = {
items: {
'item-1': { id: 'item-1', content: 'Captain America', src: 'captain.jpg' },
'item-2': { id: 'item-2', content: 'IonMan', src: 'ironman.jpg' },
'item-3': { id: 'item-3', content: 'Thor', src: 'thor.jpg' },
'item-4': { id: 'item-4', content: 'Hulk', src: 'hulk.jpg' },
'item-5': { id: 'item-5', content: 'Spiderman', src: 'spider.jpg' },
'item-6': { id: 'item-6', content: 'Groot', src: 'groot.jpg' },
'item-7': { id: 'item-7', content: 'Rocket', src: 'rocket.jpg' },
'item-8': { id: 'item-8', content: 'Thanos', src: 'thanos.png' },
},
columns: {
'column-1': {
id: 'column-1',
title: 'Heros',
itemIds: [
'item-1',
'item-2',
'item-3',
'item-4',
'item-5',
'item-6',
'item-7',
],
},
'column-2': {
id: 'column-2',
title: 'villain',
itemIds: ['item-8'],
},
},
columnOrder: ['column-1', 'column-2'],
}
avengers ๋ฆฌ๋์์ ์ด๊ธฐ state
https://github.com/atlassian/react-beautiful-dnd
<DragDropContext
onDragStart={onDrageStartHandler}
onDragUpdate={onDrageUpdateHandler}
onDragEnd={onDrageEndHandler}
>
<DndContainer>
{dndData.columnOrder.map(columnId => {
const column = dndData.columns[columnId]
const items = column.itemIds.map(itemId => dndData.items[itemId])
return <Column key={column.id} column={column} items={items} />
})}
</DndContainer>
</DragDropContext>
dnd ์์์์
const onDrageStartHandler = () => {
for (
let i = 0;
i < document.querySelectorAll('.droppable_table').length;
i++
) {
document.querySelectorAll('.droppable_table')[
i
].style.background = `rgba(255,141,217,0.2)`
}
}
TopConatiner์์ ๋๋๊ทธ์ค๋๋์ด ๋ฐ์ํ๋ column ๋ถ๋ถ์ class๋ฅผ โ.droppable_tableโ๋ก ์ ์ํด์ฃผ๊ณ dnd๊ฐ ์์๋๋ฉด ๊ทธ column ๋ฐฐ๊ฒฝ์์ ๋ณ๊ฒฝํด์ฃผ์๋ค.
dnd ์ ๋ฐ์ดํธ๋๋ ์์
const onDrageUpdateHandler = update => {
// ...
}
๋๋๊ทธ์ค๋๋ ์ค, ๋๋๊ทธ ๋์ค ๋ฐ์ํ๋ ํจ์. ์ฌ์ฉํ์ง ์์.
dnd ์ข ๋ฃ์์
const onDrageEndHandler = result => {
document.body.style.color = 'inherit'
document.body.style.background = 'inherit'
for (
let i = 0;
i < document.querySelectorAll('.droppable_table').length;
i++
) {
document.querySelectorAll('.droppable_table')[
i
].style.background = `inherit`
}
const { destination, source, draggableId } = result
if (!destination) {
return
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return
}
const start = dndData.columns[source.droppableId] // 'column-1'
const finish = dndData.columns[destination.droppableId]
if (start === finish) {
const newItemIds = Array.from(start.itemIds)
newItemIds.splice(source.index, 1)
newItemIds.splice(destination.index, 0, draggableId)
const newColumn = {
...start,
itemIds: newItemIds,
}
const newDndData = {
...dndData,
columns: {
...dndData.columns,
[newColumn.id]: newColumn,
},
}
setDndData(newDndData)
return
}
const startItemIds = Array.from(start.itemIds)
startItemIds.splice(source.index, 1)
const newStart = {
...start,
itemIds: startItemIds,
}
const finishItemIds = Array.from(finish.itemIds)
finishItemIds.splice(destination.index, 0, draggableId)
const newFinish = {
...finish,
itemIds: finishItemIds,
}
const newDndData = {
...dndData,
columns: {
...dndData.columns,
[newStart.id]: newStart,
[newFinish.id]: newFinish,
},
}
setDndData(newDndData)
return
}
๋๋๊ทธ ๊ฐ๋ฅํ ์์ดํ ์ ๋๋๊ทธํด์ ๋๋ํ๋ฉด ๋ฐ์ํ๋ ํจ์
ํจ์๊ฐ ์คํ๋ ๋, result๋ผ๋ ๊ฐ์ฒด๋ฅผ ์ ๋ฌ๋ฐ๋๋ค. result๊ฐ์ฒด ๋ด๋ถ๋ ๋ค์๊ณผ ๊ฐ์ด ์๊ฒผ๋ค.
const result = {
// ๋๋๊ทธ ํ item ID
draggableId: '',
// ๋๋๊ทธ ์์ ์์ column ์ ๋ณด
source: {
index: 2,
droppableId: '',
},
// ๋๋๊ทธ ์ข
๋ฃ ์์ column ์ ๋ณด
destination: {
index: 1,
droppableId: '',
},
}
์ด ์ ๋ณด๋ฅผ ํตํด์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฐ์ดํธํด์ฃผ์๋ค.
๋จผ์ , ๋๋๊ทธ ์ข ๋ฃ ์, ์์์ ๊ณผ ๋ชฉ์ ์ง๊ฐ ๋์ผํ ๊ฒฝ์ฐ๋ ์กฐ๊ฑด๋ฌธ ์ฒ๋ฆฌ๋ฅผ ํตํด ๋ค์ ์ฝ๋๋ฅผ ์คํํ์ง ์๊ฒ๋ ํด์ฃผ์๊ณ
์ค์ ๋ณ๊ฒฝ์ด ์ผ์ด๋ฌ์ ๊ฒฝ์ฐ, ๋์ผํ column์์ ๋ณ๊ฒฝ์ด ์ผ์ด๋ฌ์ ๋์ ๋ค๋ฅธ column์ผ๋ก ์ด๋ํ์ ๊ฒฝ์ฐ, ๋๊ฐ์ง๋ฅผ ์กฐ๊ฑด๋ฌธ์ ํตํด ๋ฐ๋ก ์ฝ๋๋ฅผ ์์ฑํด์ฃผ์๋ค.
const newItemIds = Array.from(start.itemIds)
newItemIds.splice(source.index, 1)
newItemIds.splice(destination.index, 0, draggableId)
const newColumn = {
...start,
itemIds: newItemIds,
}
const newDndData = {
...dndData,
columns: {
...dndData.columns,
[newColumn.id]: newColumn,
},
}
setDndData(newDndData)
return
๋ฆฌ์กํธ์์ ์ํ ๋ณ๊ฒฝ์ ๊ฐ์งํ๊ธฐ ์ํด์ ๊ฐ์ฒด๋ฅผ ์ง์ ๋ณ๊ฒฝํ๋ ๊ฒ์ด ์๋ ์๋ก์ด ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ hooks๋ก ๋ฃ์ด์ฃผ์ด์ผํ๋ค.
flux ํจํด
๊ทธ๋์ ๊ฐ์ฒด ๋ด๋ถ column ๋ฐฐ์ด์ ๋ณ๊ฒฝํด ์ฃผ์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ์๋ก์ด ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ source ์ ๋ณด์ destination ์ ๋ณด์ ๋ง๊ฒ ๋ฐฐ์ด์ ์ ๋ฐ์ดํธ ํด์ค ํ์, ์คํ๋ ๋ ๋ฌธ๋ฒ์ผ๋ก ๋ถ๋ณ์ฑ์ ์ ์งํ๋ฉด์ hooks๋ก ์ํ๋ฅผ ์ ๋ฐ์ดํธ ํด์ฃผ์๋ค.
const startItemIds = Array.from(start.itemIds)
startItemIds.splice(source.index, 1)
const newStart = {
...start,
itemIds: startItemIds,
}
const finishItemIds = Array.from(finish.itemIds)
finishItemIds.splice(destination.index, 0, draggableId)
const newFinish = {
...finish,
itemIds: finishItemIds,
}
const newDndData = {
...dndData,
columns: {
...dndData.columns,
[newStart.id]: newStart,
[newFinish.id]: newFinish,
},
}
setDndData(newDndData)
return
๋๋๊ทธํ item์ด ๋ค๋ฅธ column์ผ๋ก ์ด๋ํ์ ๊ฒฝ์ฐ๋ column์ ๊ฐ๊ฐ ์์ ํ๊ณ ์ ๋ฐ์ดํธํด์ฃผ์๋ค.
// column.js
const Column = ({ column, items }) => {
return (
<Container>
<Title>{column.title}</Title>
<Droppable droppableId={column.id}>
{provided => (
<ItemList
className="droppable_table"
ref={provided.innerRef}
{...provided.droppableProps}
>
{items.map((item, i) => (
<Item key={item.id} item={item} index={i} />
))}
{provided.placeholder}
</ItemList>
)}
</Droppable>
</Container>
)
}
export default Column
required property
droppableId๋ ํ์ ์์ฑ์ผ๋ก, ์ ๋ฌ๋ฐ์ column props์ id ๋ฅผ ๋ฃ์ด์ฃผ์๋ค.
react-beautiful-dnd ์ Droppable ์ปดํฌ๋ํธ๋ react ํจ์ํํ๋ฅผ ๋ฐํํ๋ค.
The React children of a <Droppable /> must be a function that returns a ReactElement.
<Droppable droppableId="droppable-1">
{(provided, snapshot) => ({
/*...*/
})}
</Droppable>
๊ทธ๋ฆฌ๊ณ provided ๋ณ์์ ์์ฑ ๋ฐ ๋ฉ์๋๋ฅผ ํจ์ ๋ด๋ถ ์ปดํฌ๋ํธ์ props๋ก ์ ๋ฌํด์ฃผ์๋ค.
์์ธํ ์ฉ๋ฒ react-beautiful-dnd Droppable ๋ฉ๋ด์ผ(๊นํ๋ธ)
// item.js
const Item = ({ item, index }) => {
return (
<Draggable draggableId={item.id} index={index}>
{(provided, snapshot) => (
<Container
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
isDragging={snapshot.isDragging}
>
{item.content}
</Container>
)}
</Draggable>
)
}
export default Item
required property
draggableId์ index ๋ํ ํ์ ์์ฑ์ผ๋ก, ์ ๋ฌ๋ฐ์ item.id์ index๋ฅผ ๋ฃ์ด์ฃผ์๋ค.
provided ๊ฐ ํ๊ฒฝ์ ๊ด๋ จ๋ ์์๋ผ๋ฉด snapshot์ ์ธํฐ๋ ์ ์ ๊ด๋ จ๋ ์์์ธ ๊ฒ ๊ฐ๋ค. ์ฌ๋ฌ ์ฉ๋ฒ์ด ์๋๋ฐ ์ฌ๊ธฐ์๋ isDragging ์์ฑ์ ๋ฃ์ด์ฃผ์๋ค.
์์ธํ ์ฉ๋ฒ react-beautiful-dnd Draggable ๋ฉ๋ด์ผ(๊นํ๋ธ)
<DndContainer>
{dndData.columnOrder.map(columnId => {
const column = dndData.columns[columnId]
const items = column.itemIds.map(itemId => dndData.items[itemId])
return <Column key={column.id} column={column} items={items} />
})}
</DndContainer>
๋จผ์ DndContainer
styled-component ๋ก ๊ฐ์ธ์ฃผ์๊ณ , column ๋ฐฐ์ด์ map ํจ์๋ฅผ ํตํด ๋๋๊ณ , ๊ฐ column์ ๋ํ item ์ ๋ณด๋ฅผ ๋ค์ items ๋ฐฐ์ด์ ๋ฃ์ด์ฃผ์๋ค.
๊ทธ๋ฆฌ๊ณ column๊ณผ items ๋ฅผ ๊ฐ์ง๊ณ Column ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌํดํด์ฃผ์๋ค.
<div>
{dndData.columns['column-1'].itemIds.map((v, i) => {
return (
<img
key={i}
style={{
borderRadius: '10px',
width: '90px',
height: '90px',
padding: '5px',
}}
src={dndData.items[v].src}
/>
)
})}
</div>
column ํ๋์ ๋ํ ๋ทฐ (์ด 2๊ฐ ์์)
column์ items ์ ๋ณด๋ค์ map ํจ์๋ฅผ ์ด์ฉํด์ ๊ฐ๊ฐ์ ์ ๋ณด๋ค์ img ํ๊ทธ ์์ฑ์ ๋ด์ ๋ฆฌํดํด์ฃผ์๋ค.
{
dndData.columns['column-1'].itemIds.length === 0 ||
dndData.columns['column-2'].itemIds.length === 0 ? (
<VSContainer>
Friend <span>โค๏ธ</span>
</VSContainer>
) : (
<VSContainer>vs</VSContainer>
)
}
๋ง์ง๋ง์ผ๋ก column ๊ณผ column ์ฌ์ด์ VS ํ ์คํธ๋ฅผ ๋ฃ์๋๋ฐ,
๊ฐ column ์ค์์ ํ๋๊ฐ item์ ๊ฐ์ง๊ณ ์์ง ์๊ฒ๋๋ฉด Friend ๋ผ๋ ํ ์คํธ๊ฐ ์ถ๋ ฅ๋๋๋ก ์ผํญ์ฐ์ฐ์๋ฅผ ํตํด ํด์ฃผ์๋ค.
์ด ์ดํ๋ฆฌ์ผ์ด์ ์ ์๋จ์ ๋๋๊ทธ์ค๋๋ ์ปจํ ์ด๋๊ฐ ์๊ณ ํ๋จ์ ๊ทธ ์ ๋ณด์ ๋ง๋ ์ด๋ฏธ์ง๊ฐ ์กด์ฌํ๋๋ฐ ์์ธ๋ฆฌ ์ด๋ฏธ์ง๋ฅผ ๋๋๊ทธ์ค๋๋ ํด์ ์ปจํ ์ด๋๋ฅผ ํ๋๋ก ํตํฉ์ํค๋ ๊ฒ์ด ๋ ๊ด์ฐฎ์๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ ํ๊ฒ ๋์๋ค.
์ฐธ๊ณ ์ฌ์ดํธ ๋กค์ฒด์ง์ง (๋กคํ ์ฒด์ค ๋ฐฐ์นํด)