칸반 보드란?
칸반(Kanban)은 '시각 신호'를 뜻하는 일본어로서 도요타 자동차 창업주가 처음 고안했다. 칸반 보드는 작업을 시각적으로 표시해 주어 프로젝트 관리를 쉽게 할수 있도록 도와준다.
카드 목록을 수직 방향으로 구성해 목록이나 카드는 드래그 앤 드롭으로 소속이나 순서를 자유롭게 변경할 수 있다.
react-dnd 패키지 설치하기
react-dnd패키지는 드래그 앤 드롭 기능을 좀 더 쉽게 구현할 수 있게 하는 패키지들 중 하나이다.
> npm i react-dnd react-dnd-html5-backend
> npm i -D @types/react-dnd
react-dnd는 리액트 컨텍스트 기반으로 설계 되었으며 제공하는 컴포넌트를 사용하기 위해선 DndProvider 컴포넌트가 최상위 컴포넌트로 동작해야 한다.
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
export default function App() {
return (
<DndProvider backend={HTML5Backend}>
/* react-dnd 기능 사용 컴포넌트 */
</DndProvider>
)
}
react-beautiful-dnd 패키지 설치하기
react-beautiful-dnd 패키지는 현재 아틀라시안사가 유지보수하는 패키지이지만 새로운 기능이 추가되고 있지는 않다. 또한 react-beautiful-dnd패키지는 현재 리액트 18을 peerDependencies로 설정하지 않고 있으므로 --legacy-peer-deps 옵션을 붙이지 않으면 설치할 수 없다.
> npm i --legacy-peer-deps react-beautiful-dnd
> npm i -D @types/react-beautiful-dnd
앱 상태를 구성하는 멤버 상태와 테스트용 컴포넌트 만들기
우선 칸반보드의 앱의 상태를 관리하기 위한 멤버상태를 구성 후 AppState와 rootReducer에 각 멤버상태를 설정한다.
- listEntities : 목록의 상태를 관리하기 위해 사용된다.
- listidOrders : 목록의 순서를 관리하기 위해 사용된다.
- cardEntities : 목록에 포함될 카드의 상태를 관리하기 위해 사용된다.
- listidCardidOrders : 어떤 목록에 카드가 속해있고 순서는 몇번째인지를 확인하기 위해 사용된다.
맴버상태를 구성 후 칸반보드를 테스트하기 위한 컴포넌트를 생성한다.
- Board :
- BoardList :
- ListCard :
CreateListForm 컴포넌트 구현하기
새로운 목록을 생성하기 위해 Board 컴포넌트에 포함될 CreateListForm 을 추가한다.
import type {FC, ChangeEvent} from 'react'
import {useState, useCallback} from 'react'
import {Icon} from '../../theme/daisyui'
import * as D from '../../data'
export type CreateListFormProps = {
onCreateList: (uuid: string, title: string) => void
}
const CreateListForm: FC<CreateListFormProps> = ({onCreateList}) => {
const [value, setValue] = useState<string>(D.randomTitleText())
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setValue(() => e.target.value)
}, [])
const addList = useCallback(() => {
onCreateList(D.randomUUID(), value)
setValue(() => D.randomTitleText())
}, [value, onCreateList])
// prettier-ignore
return (
<div className="flex p-2">
<input placeholder="title"
value={value} onChange={onChange}
className="input-xs input-bordered input input-primary" />
<Icon name="add" onClick={addList} disabled={!value.length}
className="ml-2 btn-primary" />
</div>
)
}
export default CreateListForm
추가한 CreateListForm 컴포넌트를 index.tsx 파일에 코드 추가한다.
import {useCallback} from 'react'
import {Title} from '../../components'
import CreateListForm from './CreateListForm'
export default function CopyMe() {
const onCreateList = useCallback((uuid: string, title: string) => {
console.log('onCreateList', uuid, title)
}, [])
return (
<section className="mt-4">
<Title>Board</Title>
<div className="mt-4">
<CreateListForm onCreateList={onCreateList} />
</div>
</section>
)
}
배열 대신 ids와 entities로 상태 구현하기
칸반 보드는 n개의 목록(list)이 있고 각 목록은 n개의 카드(card)를 가질 수 있다. 그리고 각 목록은 드래그 앤 드롭으로 위치를 자유롭게 옴길 수 있다. 예를들어 목록을 배열에 담으면 이러한 기능을 구현하기 까다롭다.
lists = [list1, list2, ...] // 까다로운 배열
앵귤러 프레임워크는 리덕스 기능을 구현하며 @ngrx/entity라는 패키지를 사용해 배열에 들어갈 아이템은 모두 서로를 구분할 수 있는 고유 ID값을 가진다고 가정할 수 있다.
그리고 entities란 객체를 하나 만들어, id_값: 아이템(1:1) 형태로 여러 개의 아이템을 저장한다. 이런 구조를 가지는 타입을 엔티티(entity)라고 부르겠다. 다만 타입스크립트는 기본으로 제공되는 Record 제네릭 타입을 이해해야 한다.
// redux-toolkit.js.org 사이트에서 인용한 ids와 entities의 역할
{
ids: [] // 각 항목 고유 ID, 문자열 또는 숫자
entities: {} // 엔티티 ID를 해당 엔티티 객체에 매핑하는 조회(lookup) 테이블
}
타입스크립트의 Record 타입
자바스크립트는 색인 연산자(index operator) []를 사용해 객체의 속성값을 얻어올 수 있다. 다만 속성 이름을 잘못 입력 시 오류가 발생한다.
import {makeRandomCard} '../../data'
const card = makeRandomCard()
const uuid = card['uuid'] // 색인 연산자로 객체의 'uuid' 속성 값 설정
이 때문에 타입스크립트는 객체의 속성값을 색인 연산자로 얻을 수 없다. 대신 Record란 특별한 타입을 제공한다.
// ReCord선언문(제네릭 타입으로 선언)
// K extends keyof any : 키로 사용될 수 있는 타입설정(any는 모든)
// T : 객체의 값의 타입을 의미하며 모든 값이 동일한 T타입을 가지게 된다.
type Record<K extends keyof any, T> = {
// Mapped Type은 Typescript에서 객체의 프로퍼티를 동적으로 정의할 수 있게 한다.
// [P in K]는 K 타입의 각 요소를 돌며 객체의 키로 사용하겠다는 의미이다.
// K요소는 키가되고, 그 키들의 값은 T가 된다.
[P in K]: T;
};
공통으로 사용하는 타입 구현하기
멤버 상태들이 공통으로 사용하는 타입이 있으므로 먼저 commonTypes.ts를 추가한다.
import type {ICard} from '../data'
export type UUID = string
export type List = { // 목록
uuid: UUID
title: string
}
export type Card = ICard
export type CardidListid = { // 카드와 목록을 매핑
cardid: UUID
listid: UUID
}
export type listidCardid = CardidListid
export type ListidCardidS = {listid: UUID; cardids: UUID[]}
export type CardidListidIndex = CardidListid & {
index: number // 카드목록매핑 타입에 index를 추가
}
listidOrders 멤버 상태 구현하기
listidOrders 멤버 상태는 생성한 목록의 uuid값을 배열에 담아 웹 페이지에 어떤 순서로 표시할 것인지 결정하는 역할을 한다.
type은 uuid값을 담을 배열을 선언한 후 목록의 추가, 드래그 앤 드롭에 의한 순서변경, 삭제할 수 있으므로 대응할 3가지 액션을 설정한다.
import type {Action} from 'redux'
import type {UUID} from '../commonTypes'
export * from '../commonTypes'
export type State = UUID[] // UUID값을 배열로 갖고있음을 암시
export type SetListidOrders = Action<'@listidOrders/set'> & {
payload: State
}
export type AddListidToOrders = Action<'@listidOrders/add'> & {
payload: UUID
}
export type RemoveListidFromOrders = Action<'@listidOrders/remove'> & {
payload: UUID
}
export type Actions = SetListidOrders | AddListidToOrders | RemoveListidFromOrders
앞서 type에서 선언한 3가지 액션타입의 액션 생성기 코드를 추가한다.
import type * as T from './types'
export const setListidOrders = (payload: T.State): T.SetListidOrders => ({
type: '@listidOrders/set',
payload
})
export const AddListidToOrders = (payload: T.UUID): T.AddListidToOrders => ({
type: '@listidOrders/add',
payload
})
export const RemoveListidFromOrders = (payload: T.UUID): T.RemoveListidFromOrders => ({
type: '@listidOrders/remove',
payload
})
액션 생성기로 생성한 3개의 액션을 대응하는 리듀서를 구현한다.
import * as T from './types'
const initialAppState: T.State = []
export const reducer = (state: T.State = initialAppState, action: T.Actions) => {
switch (action.type) {
case '@listidOrders/set': //
return (state = action.payload)
case '@listidOrders/add': // 목록의 추가
return [...state, action.payload]
case '@listidOrders/remove': // 목록의 삭제
return state.filter(uuid => uuid !== action.payload)
}
return state
}
listEntities 멤버 상태 구현하기
listEntities 멤버 상태는 commonTypes.ts 파일에 선언했던 List 타입 객체들을 엔티티 방식으로 저장하는 역할을 수행한다. List타입은 string 타입의 uuid속성과 카드 목록의 용도를 구분할 수 있도록 string 타입의 title 속성을 가지고 있다.
export type List = {
uuid : UUID
title: string
}
타입에 카드목록을 추가하거나 삭제할 수 있는 액션을 2개 정의한다.
import type {Action} from 'redux'
import type {List as commonList} from '../commonTypes'
export type List = commonList
export type State = Record<string, List>
export type AddListAction = Action<'@listEntities/add'> & {
payload: commonList
}
export type RemoveListAction = Action<'@listEntities/remove'> & {
payload: string
}
export type Actions = AddListAction | RemoveListAction
다음으로 type에서 선언한 2개의 액션 타입에 대한 각각의 액션 생성기 코드를 추가한다.
import type * as T from './types'
export const addList = (payload: T.List): T.AddListAction => ({
type: '@listEntities/add',
payload
})
export const removeList = (payload: string): T.RemoveListAction => ({
type: '@listEntities/remove',
payload
})
리듀서 구현 시 액션을 통해 State에 포함된 Record<string, List> 객체에 값을 추가하거나 삭제해 주는 과정을 준비해야 한다. 목록을 생성하는 @listEntities/add 액션 구현 시 새로운 목록은 action.payload에 담겨있다.
state는 Record<UUID, List> 타입의 엔티티이므로 state[uuid] = list 형태로구현할 수 있다. 다만 리듀서에서는 매개변수의 수정은 불순함수이므로 문제가 발생한다. 따라서 깊은복사인 {...state, [uuid]: card} 형태로 구현해야 한다.
state[uuid] = list; return state // 리듀서는 순수 함수만 가능하므로 불순함수는 불가하다.
return {...state, [uuid]: list} // 순수함수로 올바른 구현
목록을 제거하는 @listEntities/remove 액션은 제거할 목록의 uuid값은 action.payload에 담겨있다. 타입스크립트에서 객체 특정 속성을 삭제하 땐 delete 연산자를 통해 삭제할 수 있다. 다만 리듀서에는 불순함수이므로 잘못된 구현이므로 깊은 복사 방식으로 구현해야 한다.
delete state[uuid]; return state // 불순함수이므로 잘못된 구현(매개변수값의 변경)
const newstate = {...State} // 깊은복사 방식을 통한 순수함수 구현
delete newState[uuid]
return newState
위 내용을 토대로 순수함수만을 이용하여 리듀서 코드를 추가한다.
import * as T from './types'
const initialAppState: T.State = {}
export const reducer = (state: T.State = initialAppState, action: T.Actions) => {
switch (action.type) {
case '@listEntities/add':
return {...state, [action.payload.uuid]: action.payload}
case '@listEntities/remove':
const newState = {...state}
delete newState[action.payload]
return newState
}
return state
}
앞서 만든 listidOrders와 listEntities 멤버 상태를 Board 컴포넌트에 적용한다. listidOrders는 list의 순서를 설정 목록이고 listEntities는 목록의 Object를 key:value로 관리하기 위한 형태이다.
import {useCallback} from 'react'
import {useDispatch} from 'react-redux'
import {Title} from '../../components'
import CreateListForm from './CreateListForm'
import * as LO from '../../store/listidOrders'
import * as L from '../../store/listEntities'
export default function CopyMe() {
const dispatch = useDispatch()
const onCreateList = useCallback(
(uuid: string, title: string) => {
const list = {uuid, title}
dispatch(LO.addListidToOrders(list.uuid)) // 배열을 관리
dispatch(L.addList(list))
},
[dispatch]
)
return (
<section className="mt-4">
<Title>Board</Title>
<div className="mt-4">
<CreateListForm onCreateList={onCreateList} />
</div>
</section>
)
}
BoardList 컴포넌트 구현하기
앞선 listidOrders와 listEntities는 상태에 목록정보를 생성하는 기능을 제공했기에 화면상에 변화는 없다. 이번에는 화면상에 추가된 상태정보의 목록을 추가/삭제하는 BoardList 컴포넌트를 추가해보자.
import type {FC} from 'react'
import type {List} from '../../store/commonTypes'
import {Icon} from '../../theme/daisyui'
export type BoardListProps = {
list: List
onRemoveList?: () => void
}
const BoardList: FC<BoardListProps> = ({list, onRemoveList, ...props}) => {
return (
<div {...props} className="p-2 m-2 border border-gray-300 rounded-lg">
<div className="flex justify-between mb-2">
<p className="w-32 text-sm font-bold underline line-clamp-1">{list.title}</p>
<div className="flex justify-between ml-2">
<Icon name="remove" className="btn-error btn-xs" onClick={onRemoveList} />
</div>
</div>
</div>
)
}
export default BoardList
listEntities 객체에 담긴 목록을 화면에 표현하기 위해서는 타입을 기존 List에서 List[] 타입으로 변경해야 한다.
import type {AppState} from '../store'
import type {List} from '../store/commonTypes'
import type as L from '../store/listEntities'
const lists = useSelector<AppState, List[]>(({listEntities}) => ???)
그리고 각 목록의 순서는 listidOrders에 담겨 있다.
import * as LO from '../store/listidOrders'
const listidOrders = useSelector<AppState, LO.State>(({listidOrders}) => listidOrders)
결과적으로 List[] 타입 배열은 listidOrders를 통해 uuid의 순서를 결정하고, 각 uuid에 해당하는 목록을 listEntities에서 얻어올 수 있다.
const lists = useSelector<AppState, List[]>(({listidOrders, listEntities}) =>
listidOrders.map(uuid => listEntities[uuid])
)
위 과정을 통해 BoardList를 화면에 표시해 줄 수 있으며 해당 정보를 Baord에 내용을 추가해보자.
import {useCallback, useMemo} from 'react'
import {useSelector, useDispatch} from 'react-redux'
import {Title} from '../../components'
import CreateListForm from './CreateListForm'
import BoardList from '../BoardList'
import type {AppState} from '../../store'
import type {List} from '../../store/commonTypes'
import * as LO from '../../store/listidOrders'
import * as L from '../../store/listEntities'
export default function CopyMe() {
const dispatch = useDispatch()
const lists = useSelector<AppState, List[]>(({listidOrders, listEntities}) =>
listidOrders.map(uuid => listEntities[uuid])
)
const onCreateList = useCallback(
(uuid: string, title: string) => {
const list = {uuid, title}
dispatch(LO.addListidToOrders(list.uuid))
dispatch(L.addList(list))
}, [dispatch])
const onRemoveList = useCallback((listid: string) => () => {
dispatch(L.removeList(listid))
dispatch(LO.removeListidFromOrders(listid))
}, [dispatch])
const children = useMemo(() =>
lists.map(list => (
<BoardList key={list.uuid} list={list} onRemoveList={onRemvoeList(list.uuid)} />
)), [lists, onRemvoeList])
return (
<section className="mt-4">
<Title>Board</Title>
<div className="flex flex-wrap p-2 mt-4">
{children}
<CreateListForm onCreateList={onCreateList} />
</div>
</section>
)
}
리덕스 기능을 커스텀 훅으로 만들기
Board컴포넌트 내에 있는 리덕스의 기능을 분리해 코드의 복잡성을 줄이기 위해 useLists 커스텀 훅을 추가한다.
import {useCallback} from 'react'
import {useSelector, useDispatch} from 'react-redux'
import type {AppState} from './AppState'
import {List} from '../store/commonTypes'
import * as LO from '../store/listidOrders'
import * as L from '../store/listEntities'
export const useLists = () => {
const dispatch = useDispatch()
const lists = useSelector<AppState, List[]>(({listidOrders, listEntities}) =>
listidOrders.map(uuid => listEntities[uuid])
)
const onCreateList = useCallback(
(uuid: string, title: string) => {
const list = {uuid, title}
dispatch(LO.addListidToOrders(list.uuid)) // 배열을 관리
dispatch(L.addList(list))
}, [dispatch])
const onRemvoeList = useCallback((listid: string) => () => {
dispatch(L.removeList(listid))
dispatch(LO.removeListidFromOrders(listid))
}, [dispatch])
return {lists, onCreateList, onRemoveList}
}
기존 Board 컴포넌트에 useLists 훅을 적용해 코드를 간결하게 만든다.
...(생략) ...
import {useLists} from '../../store/useLists'
export default function CopyMe() {
const {lsits, onRemoveList, oncreateList} = useLists()
...(생략)....
}
ListCatd 컴포넌트 구현하기
목록에 추가될 카드목록 컴포넌트를 추가하자.
import type {FC} from 'react'
import type {ICard} from '../../data'
import {Div, Avatar} from '../../components'
import {Icon} from '../../theme/daisyui'
export type UserCardProps = {
card: ICard
onRemove?: () => void
onClick?: () => void
}
const Card: FC<UserCardProps> = ({card, onRemove, onClick}) => {
const {image, writer} = card
const {avatar, name, jobTitle} = writer
return (
<Div className="m-2 border shadow-lg rounded-xl" width="10rem" onClick={onClick}>
<Div src={image} className="relative h-20">
<Icon
name="remove"
className="absolute right-1 top-1 btn-primary btn-xs"
onClick={onRemove}
/>
</Div>
<Div className="flex flex-col p-2">
<Div minHeight="4rem" height="4rem" maxHeight="4rem">
<Div className="flex flex-row items-center">
<Avatar src={avatar} size="2rem" />
<Div className="ml-2">
<p className="text-xs font-bold">{name}</p>
<p className="text-xs text-gray-500">{jobTitle}</p>
</Div>
</Div>
</Div>
</Div>
</Div>
)
}
export default Card
cardEntities 멤버 상태 구현하기
cardEntities를 관리하기 위한 타입선언, 액션생성기, 리듀서를 순차적으로 추가하자. 이는 카드의 상태를 관리하고 카드를 추가/삭제하기 위해 사용된다.
export type ICard = { // ICard인터페이스이며 card 타입의 형태이다.
uuid: string
writer: IUser
image: string
title: string
paragraphs: string
dayMonthYearDate: string
relativeDate: string | null
}
import type {Action} from 'redux'
import type {Card, UUID} from '../commonTypes'
export * from '../commonTypes'
// 상태설정
export type State = Record<UUID, Card>
// 카드 추가
export type AddCardAction = Action<'@cardEntities/add'> & {
payload: Card
}
// 카드 삭제
export type RemoveCardAction = Action<'@cardEntities/remove'> & {
payload: UUID
}
export type Actions = AddCardAction | RemoveCardAction
import type * as T from './types'
// 액션 생성기
export const AddCard = (payload: T.Card): T.AddCardAction => ({
type: '@cardEntities/add',
payload
})
export const removeCard = (payload: T.UUID): T.RemoveCardAction => ({
type: '@cardEntities/remove',
payload
})
import * as T from './types'
const initialAppState: T.State = {}
// 리듀서
export const reducer = (state: T.State = initialAppState, action: T.Actions) => {
switch (action.type) {
case '@cardEntities/add': // 카드 추가
return {...state, [action.payload.uuid]: action.payload}
case '@cardEntities/remove': // 카드 삭제
const newState = {...state}
delete newState[action.payload]
return newState
}
return state
listidCardidOrders 멤버 상태 구현하기
카드는 목록에 추가/삭제, 순서변경(drag&drop)이 가능해야 한다. 그러기 위해 listidCardidOrers 멤버상태 구현하며 Record<리스트_uuid, 카드_uuid[]> 타입의 엔티티를 갖는다.
방금전과 마찬가지로 타입선언, 액션 생성기, 리듀서 순으로 코드를 추가하자.
export type ListidCardidS = {listid: UUID; cardids: UUID[]}
import type {Action} from 'redux'
import * as CT from '../commonTypes'
export * from '../commonTypes'
// 타입 선언
export type State = Record<CT.UUID, CT.UUID[]>
export type SetListidCardids = Action<'@listidCardids/set'> & { // 카드목록 일괄등록
payload: CT.ListidCardidS
}
export type RemoveListidAction = Action<'@listidCardids/remove'> & { // 카드목록 일괄삭제
payload: CT.UUID
}
export type PrependCardidToListidAction = Action<'@listidCardids/prependCardid'> & {
payload: CT.CardidListid // 카드를 목록 맨 앞에 추가
}
export type AppendCardidToListidAction = Action<'@listidCardids/appendCardid'> & {
payload: CT.CardidListid // 카드를 목록 맨 뒤에 추가
}
export type RemoveCardidFromListidAction = Action<'@listidCardids/removeCardid'> & {
payload: CT.CardidListid // 목록의 카드 삭제
}
export type Actions =
| SetListidCardids
| RemoveListidAction
| PrependCardidToListidAction
| AppendCardidToListidAction
| RemoveCardidFromListidAction
import * as T from './types'
const initialAppState: T.State = {}
export const reducer = (state: T.State = initialAppState, action: T.Actions) => {
switch (action.type) {
case '@listidCardids/set':
// [action.payload.listid] : key, action.payload.cardids : value
// return {...state, "신규키" : [1,2,3]} <-- 아래와 동일한 결과
return {...state, [action.payload.listid]: action.payload.cardids}
case '@listidCardids/remove':
const newState = {...state}
delete newState[action.payload]
return newState
case '@listidCardids/prependCardid': {
const cardids = state[action.payload.listid]
return {...state, [action.payload.listid]: [action.payload.cardid, ...cardids]}
}
case '@listidCardids/appendCardid': {
const cardids = state[action.payload.listid]
return {...state, [action.payload.listid]: [...cardids, action.payload.cardid]}
}
case '@listidCardids/removeCardid': {
const cardids = state[action.payload.listid]
return {
...state,
[action.payload.listid]: cardids.filter(id => id !== action.payload.cardid)
}
}
}
return state
}
마지막으로 useLists 커스텀 훅에 cardEntities와 listidCardidOrders 기능을 추가한다.
...(생략)...
import * as C from '../store/cardEntities'
import * as LC from '../store/listidcardidOrders'
export const useLists = () => {
const lists = useSelector<AppState, List[]>(({listidOrders, listEntities}) =>
listidOrders.map(uuid => listEntities[uuid])
)
const listidCardidOrders = useSelector<AppState, LC.State>( // 추가
({listidCardidOrders}) => listidCardidOrders
)
const onCreateList = useCallback(
(uuid: string, title: string) => {
const list = {uuid, title}
dispatch(LO.addListidToOrders(uuid))
dispatch(L.addList(list))
dispatch(LC.setListidCardids({listid: list.uuid, cardids: []})) // 추가
},
[dispatch]
)
const onRemoveList = useCallback(
(listid: string) => () => {
listidCardidOrders[listid].forEach(cardid => { // 추가
dispatch(C.removeCard(cardid))
})
dispatch(LC.removeListid(listid)) // 추가
dispatch(L.removeList(listid))
dispatch(LO.removeListidFromOrders(listid))
},
[dispatch, listidCardidOrders] // listidCardidOrders 추가
)
return {lists, onCreateList, onRemoveList}
}
useCard 커스텀 훅 만들기
useLists 커스텀 훅과 마찬가지로 카드의 경우도 소스를 간결하게 관리하기 위해 useCards 커스텀 훅을 추가한다.
import {useCallback} from 'react'
import {useDispatch, useSelector} from 'react-redux'
import type {AppState} from './AppState'
import type {Card, UUID} from './commonTypes'
import * as C from '../store/cardEntities'
import * as LC from '../store/listidcardidOrders'
import * as D from '../data'
export const useCards = (listid: UUID) => {
const dispatch = useDispatch()
const cards = useSelector<AppState, Card[]>(({cardEntities, listidCardidOrders}) =>
listidCardidOrders[listid].map(uuid => cardEntities[uuid])
)
const onPrependCard = useCallback(() => {
const card = D.makeRandomCard()
dispatch(C.AddCard(card))
dispatch(LC.prependCardidToListid({listid, cardid: card.uuid}))
}, [dispatch])
const onAppendCard = useCallback(() => {
const card = D.makeRandomCard()
dispatch(C.AddCard(card))
dispatch(LC.appendCardidToListid({listid, cardid: card.uuid}))
}, [dispatch])
const onRemoveCard = useCallback(
(uuid: UUID) => () => {
dispatch(C.removeCard(uuid))
dispatch(LC.removeCardIdFromListId({listid, cardid: uuid}))
},
[dispatch]
)
return {cards, onPrependCard, onAppendCard, onRemoveCard}
}
useCards를 BoardList 컴포넌트에 추가해 카드관리 기능을 구현한다.
import type {FC} from 'react'
import type {List} from '../../store/commonTypes'
import {useMemo} from 'react'
import {Div} from '../../components'
import {Icon} from '../../theme/daisyui'
import ListCard from '../ListCard'
import {useCards} from '../../store/useCards'
export type BoardListProps = {
list: List
onRemoveList?: () => void
}
const BoardList: FC<BoardListProps> = ({list, onRemoveList, ...props}) => {
const {cards, onPrependCard, onAppendCard, onRemoveCard} = useCards(list.uuid)
const children = useMemo(
() =>
cards.map((card, index) => (
<ListCard key={card.uuid} card={card} onRemove={onRemoveCard(card.uuid)} />
)),
[cards, onRemoveCard]
)
return (
<Div {...props} className="p-2 m-2 border border-gray-300 rounded-lg">
<div className="flex justify-between mb-2">
<p className="w-32 text-sm font-bold underline line-clamp-1">{list.title}</p>
</div>
<div className="flex justify-between ml-2">
<Icon name="remove" className="btn-error btn-xs" onClick={onRemoveList} />
<div className="flex">
<Icon name="post_add" className="btn-success btn-xs" onClick={onPrependCard} />
<Icon name="playlist_add" className="ml-2 btn-success btn-xs" onClick={onAppendCard} />
</div>
</div>
<div className="flex flex-col p-2">{children}</div>
</Div>
)
}
export default BoardList
'FrontEnd > React' 카테고리의 다른 글
[React] 처음 만나는 리액트 라우터 (1) | 2024.12.27 |
---|---|
[React] 트렐로 따라 만들기 (2) (0) | 2024.12.20 |
[React] 리덕스 미들웨어 이해하기 (0) | 2024.12.13 |
[React] 리듀서 활용하기 (0) | 2024.12.11 |
[React] 리덕스 기본 개념 이해하기 (2) | 2024.12.10 |