react-dnd의 useDrop, useDrag 훅 알아보기
react-dnd 패키지의 기능으로 제공되는 드래그 앤 드롭으로 기능인 useDrop 훅, useDrag 훅을 알아보자.
useDrop 훅은 튜플 타입 반환값에서 두번 째 멤버변수인 drop 함수를 얻어 사용하는게 기본 사용법이며 accept는 대상을 구분하는 용도로 사용되는 문자열이다. 또한 drop함수를 호출해 사용할 수도 있다.
// useDrop 훅의 기본 사용법
const [, drop] = useDrop(() => ({
accept: 'card'
})
<div ref={(node) => drop(node)} /> // ref를 통해 drop함수를 원하는 요소설정
// drop 함수를 호출하는방식으로 구현
const divRef = useRef<HTMLDivElement>(null)
drop(divRef)
react-dnd의 useDrag 훅의 사용법은 아래와 같다. useDrop, useDrop의 type(드롭타겟)은 동일해야 상호작용이 가능하다.
const [{isDragging}, drag] = useDrag({ //{isDragging} 드래그상태 추적, drag: 드래그기능DOM연결
type: 'card', // 드롭타겟 설정
item: () => { // 드래그가 시작될 때 전달할 데이터를 반환
return {id, index} // 드래그 상태와 드롭 타겟간의 통신에 사용되는 데이터
},
collect: (monitor: any) => ({ // 드래그 상태를 추적하고 필요한 데이터를 반환
isDragging: monitor.isDragging() // 현재 요소가 드래그 중인지 반환
})
})
const opacity = isDragging ? 0 : 1 // 드래그 중일시 불투명도 위한 설정
drag(drop(ref)) // ref요소를 드래그&드롭 가능하게 설정
return (
<div ref={ref} style={{...style, opactiry}} data-handler-id={handlerId}>
{text}
</div>
)
ListDragable 컴포넌트 구현하기
앞서본 Drag&Drop 훅 설정을 토대로 ListDraggable 컴포넌트를 추가한다. 하단의 소스의 실행순서를 확인하자.
- 컴포넌트 초기화 : useRef로 <div> DOM 참조 생성 및 useDrag와 useDrop으로 드래그/드롭 설정
- 드래그 시작 : 사용자가 항목을 클릭하고 드래그하면 useDrag의 item 함수가 호출하고 isDragging상태 true로 변경
- 드래그 중 : 항목이 다른 항목 위로 이동하면 useDrop의 hover 함수 호출되며 onMove함수로 인덱스를 변경
- 드래그 종료 : 항목을 놓으면 상태가 업데이트되고 드래그 종료
- 렌더링 : 항목의 스타일 및 드래그 상태 반영(opacity 등)
import type {FC} from 'react'
import {useRef} from 'react'
import type {DivProps} from './Div'
import {useDrag, useDrop} from 'react-dnd'
import type {Identifier} from 'dnd-core'
export type MoveFunc = (dragIndex: number, hoverIndex: number) => void
export type ListDraggableProps = DivProps & {id: any; index: number; onMove: MoveFunc}
interface DragItem {index: number, id: string, type: string}
export const ListDraggable: FC<ListDraggableProps> = ({id, index, onMove, style, className, ...props}) => {
const ref = useRef<HTMLDivElement>(null)
// 드래그 시 리스트 항목 데이터를 제공하고, 드래그 상태를 추적.
const [{isDragging}, drag] = useDrag({
type: 'list',
item: () => { // 드래그가 시작될 때 전달할 데이터를 정의
return {id, index}
},
collect: (monitor: any) => ({ // 현재 드래그 상태를 모니터링
isDragging: monitor.isDragging() // 해당 항목이 그래그중인지 여부
})
})
// 다른 리스트 항목이 자신 위로(hover) 드래그되었을 때 발생, 인덱스를 비교해 위치를 업데이트.
const [{handlerId}, drop] = useDrop<DragItem, void, {handlerId: Identifier | null}>({
accept: 'list',
collect(monitor) { // 드롭상태 모니터링(handlerId: 현재 드롭 타켓에 할당된 핸들러ID)
return { handlerId: monitor.getHandlerId() }
},
hover(item: DragItem) { // 드래그된 항목이 드롭 타겟 위로 올라갈 때 발생
if (!ref.current) return
const dragIndex = item.index // 드래그중인 인덱스
const hoverIndex = index // 현대 드롭 타켓 인덱스
if (dragIndex === hoverIndex) return
onMove(dragIndex, hoverIndex) // 현재 두 항목의 위치를 업데이트하는 함수
item.index = hoverIndex // item.index를 hoverIndex로 바꿔 다음 드래그에서 최신상태 유지
}
})
const opacity = isDragging ? 0 : 1 // 드래그 중 투명도 변경
drag(drop(ref)) // 드래그와 드랍을 연결
return (
<div ref={ref} {...props} className={[className, 'cursor-move'].join(' ')} style={{...style, opacity}} data-handler-id={handlerId} />
)
}
BoardList에 ListDraggable 컴포넌트를 추가하자. ListDraggable은 index와 onMoveList 함수를 Board로부터 얻어오기 때문에 이 2개 속성을 추가로 설정해야 한다.
...(생략)...
import type {MoveFunc} from '../../components'
import {ListDraggable} from '../../components'
export type BoardListProps = {list: List, RemoveList?: () => void
index: number, onMoveList: MoveFunc // 추가
}
const BoardList: FC<BoardListProps> = ({list,onRemoveList,
index, onMoveList, // 추가
...props
}) => {
...(생략)...
return (
<ListDraggable id={list.uuid} index={index} onMove={onMoveList}>
...(생략)...
</ListDraggable>
)
}
export default BoardList
useLists 커스텀 훅의 ListDraggable 컴포넌트에 추가한 onMoveList 속성을 Board로부터 얻기위해 코드를 추가한다.
...(생략)...
export const useLists = () => {
...(생략)...
const listidOrders = useSelector<AppState, LO.State>(({listidOrders}) => listidOrders)
...(생략)...
const onMoveList = useCallback(
(dragIndex: number, hoverIndex: number) => {
const newOrders = listidOrders.map((item, index) =>
index === dragIndex
? listidOrders[hoverIndex]
: index === hoverIndex
? listidOrders[dragIndex]
: item
)
dispatch(LO.setListidOrders(newOrders))
},
[dispatch, listidOrders]
)
return {lists, onCreateList, onRemoveList, onMoveList}
}
마지막으로 Board 컴포넌트의 코드를 수정해 드래그앤 드롭이 실행가능하게 변경한다. ListDraggable에서 drag&drop을 선언했지만 Board컴포넌트에도 drop영역을 설정하는 이유는 드래그와 드롭의 책임과 동작 영역이 서로 다르기 때문이다.
ListDraggable컴포넌트는 목록을 드래그 할 수 있도록 설정하기 때문에 개별 항목의 드롭 영역을 설정한다. Board 컴포넌트는 목록 전체를 포함하는 큰 영역을 드롭 영역으로 설정한다. 즉 사용자가 리스트를 드래그해 전체 보드 영역 내에서만 작업하도록 제한한다.
import {useMemo, useRef} from 'react'
import {useDrop} from 'react-dnd'
import {Title} from '../../components'
import {useLists} from '../../store/useLists'
import BoardList from '../BoardList'
import CreateListForm from './CreateListForm'
export default function Board() {
// useDrop type인 list의 드랍가능한 가장 큰 영역을 설정하기 위함
const divRef = useRef<HTMLDivElement>(null)
const [, drop] = useDrop({
accept: 'list'
})
drop(divRef)
const {lists, onCreateList, onRemoveList, onMoveList} = useLists()
const children = useMemo(
() =>
lists.map((list, index) => (
<BoardList
key={list.uuid}
list={list}
onRemoveList={onRemoveList(list.uuid)}
index={index}
onMoveList={onMoveList}
/>
)),
[lists, onRemoveList, onMoveList]
)
return (
<section className="mt-4">
<Title>Board</Title>
<div className="flex flex-wrap p-2 mt-4">
{children}
<CreateListForm onCreateList={onCreateList} />
</div>
</section>
)
}
ListDroppable 컴포넌트 구현하기
ListDraggable 컴포넌트와 마찬가지로 Board컴포넌트에 선언된 drop을 ListDroppable 컴포넌트를 분리해 Board 컴포넌트에 추가한다.
import type {FC} from 'react'
import {useRef} from 'react'
import type {DivProps} from './Div'
import {useDrop} from 'react-dnd'
export type ListDroppableProps = DivProps & {}
export const ListDroppable: FC<ListDroppableProps> = ({...props}) => {
const divRef = useRef<HTMLDivElement>(null)
const [, drop] = useDrop({
accept: 'list'
})
drop(divRef)
return <div ref={divRef} {...props} />
}
import {useMemo, useRef} from 'react'
import {useDrop} from 'react-dnd'
import {Title} from '../../components'
import {useLists} from '../../store/useLists'
import BoardList from '../BoardList'
import CreateListForm from './CreateListForm'
import {ListDroppable} from '../../components'
export default function Board() {
const {lists, onCreateList, onRemoveList, onMoveList} = useLists()
const children = useMemo(
() =>
lists.map((list, index) => (
<BoardList
key={list.uuid}
list={list}
onRemoveList={onRemoveList(list.uuid)}
index={index}
onMoveList={onMoveList}
/>
)),
[lists, onRemoveList, onMoveList]
)
return (
<section className="mt-4">
<Title>Board</Title>
<ListDroppable className="flex flex-row p-2 mt-4">
<div className="flex flex-wrap p-2 mt-4">
{children}
<CreateListForm onCreateList={onCreateList} />
</div>
</ListDroppable>
</section>
)
}
react-beautful-dnd 패키지 이해하기
react-beautiful-dnd 패키지는 DragDropcontext와 Droppable, Draggable 컴포넌트를 제공한다. 이 컴포넌트를 통해 목록에 포함되어있는 카드를 Drag&Drop을 통해 옮기는 기능을 구현해보도록 하겠다.
이 패키지 또한 컨텍스트를 기반으로 하고 있으며 기본 사용법은 아래와 같다.
import { DragDropContext, Droppable, Draggable } form 'react-beautiful-dnd"
import type {DropResult} from 'react-beautiful-dnd'
const onDragEnd = (result: DropResult) => {}
<DragDropContext onDragEnd={onDragEnd}>
/* Droppable과 Draggable을 사용하는 컴포넌트 */
</DragDropContext>
이 코드를 Board컴포넌트에 추가한다. Listdropable 밖을 DragDropContext가 동작하기 위해서는 onDragEnd라는 콜백함수를 onDragend속성에 추가해야 한다.
import {DragDropContext} from 'react-beautiful-dnd'
...(생략)...
export default function Board() {
const {lists, onCreateList, onRemoveList, onMoveList, onDragEnd} = useLists() // onDragEnd 추가
const children = useMemo(
() =>
lists.map((list, index) => (
<BoardList
key={list.uuid}
list={list}
onRemoveList={onRemoveList(list.uuid)}
index={index}
onMoveList={onMoveList}
/>
)),
[lists, onRemoveList, onMoveList]
)
return (
<section className="mt-4">
<Title>Board</Title>
<DragDropContext onDragEnd={onDragEnd}> // 추가
<ListDroppable className="flex flex-row p-2 mt-4">
<div className="flex flex-wrap p-2 mt-4">
{children}
<CreateListForm onCreateList={onCreateList} />
</div>
</ListDroppable>
</DragDropContext>
</section>
)
}
CardDraggable 컴포넌트 구현하기 (ListCard 컴포넌트에 적용)
react-beautiful-dnd 패키지는 Draggable 컴포넌트를 제공하며 사용법이 독특하므로 CardDraggable 컴포넌트로 분리해 구현하자.
import type {FC, PropsWithChildren} from 'react'
import {Draggable} from 'react-beautiful-dnd'
// draggableId : 카드의 uuid, index: 카드순서
export type CardDraggableProps = {draggableId: string; index: number}
export const CardDraggable: FC<PropsWithChildren<CardDraggableProps>> = ({
draggableId,
index,
children
}) => {
return (
<Draggable draggableId={draggableId} index={index}>
{provided => {
return (
<div
// DOM 노드를 드래그 상태와 연결하는데 사용되는 ref
ref={provided.innerRef}
// 드래그 가능한 항목을 올바르게 렌더링하기 위해 필요한 속성들
{...provided.draggableProps}
// 드래그를 시작할 때 필요한 속성을 제공하며 이속성을 전달해야 드래그 핸들이 활성화된다.
{...provided.dragHandleProps}>
{children}
</div>
)
}}
</Draggable>
)
}
CardDroppable 컴포넌트 구현하기 (BoardList 컴포넌트에 적용)
react-beautiful-dnd 패키지는 Droppable 컴포넌트를 제공하며 이 또한 사용법이 독특하므로 CardDroppable 컴포넌트로 분리해 구현하자.
import type {FC, PropsWithChildren} from 'react'
import {Droppable} from 'react-beautiful-dnd'
// droppableId: 목록의 uuid
export type CardDroppableProps = {
droppableId: string
}
export const CardDroppable: FC<PropsWithChildren<CardDroppableProps>> = ({
droppableId,
children
}) => {
return (
// Droppable : 컴포넌트를 감싸 드래그된 항목을 드롭할 수 있는 영역을 생성
<Droppable droppableId={droppableId}>
{provided => (
<div
ref={provided.innerRef} // DOM 요소와 react-beautiful-dnd의 드롭 상태를 연결
{...provided.droppableProps} // 드롭가능한 영역의 필요한 props
className="flex flex-col p-2">
{provided.placeholder} // 드래그 중인 항목이 드롭될 공간을 유지하고 상태를 업데이트
{children}
</div>
)}
</Droppable>
)
}
배열 관련 유틸리티 함수 구현하기
DragDropContext 컴포넌트가 요구하는 onDragEnd 속성 설정할 콜백함수에 적용할 유틸리티 함수를 추가한다.
// 현재 카드의 위치를 변경할 때 사용된다.
export const swapItemsInArray = <T>(array: T[], index1: number, index2: number) =>
array.map((item, index) =>
index === index1 ? array[index2] : index === index2 ? array[index1] : item
)
// 특정된 카드를 삭제할 때 사용한다.
export const removeItemAtIndexInArray = <T>(array: T[], removeIindex: number) =>
array.filter((notUsed, index) => index !== removeIindex)
// 현재 목록이 아닌 다른목록의 카드목록에 새 카드를 추가한다.
export const insertItemAtIndexInArray = <T>(array: T[], insertIndex: number, item: T) => {
const before = array.filter((item, index) => index < insertIndex)
const after = array.filter((item, index) => index >= insertIndex)
return [...before, item, ...after]
}
onDragEnd 콜백 함수 구현하기
'FrontEnd > React' 카테고리의 다른 글
[React] Outlet 컴포넌트와 중첩 라우팅 (0) | 2024.12.27 |
---|---|
[React] 처음 만나는 리액트 라우터 (1) | 2024.12.27 |
[React] 트렐로 따라 만들기 (1) (1) | 2024.12.17 |
[React] 리덕스 미들웨어 이해하기 (0) | 2024.12.13 |
[React] 리듀서 활용하기 (0) | 2024.12.11 |