저번엔 MVVM에 관련한 글을 썼는데, 이번엔 MVVM에 Recoil을 어떻게 무엇을 이용해서 MVVM을 구현했는지 말해보려한다.
Recoil은 물론 나온지 얼마 되지 않았고, 점점 발전해 나가고 있는 상태관리 라이브러리이다.
상당히 경험해보니 쉽고, 접근하기도 어렵지 않아서 지금도 작은 프로젝트에서 자주 사용하고 있다.
Redux, Redux-saga 등을 이미 사용하고 있는 프로젝트에는 더 Recoil이 발전하면 사용하길 권한다. 그리고 새로 입문하기에도 좋은 라이브러리인 것 같다.
여기서는 Recoil의 기초보단 MVVM에 관련해서 어떻게 썼는지를 집중적으로 작성할 예정이다.
Atom + Selector 의 구조이고, atom은 state(상태)를 뜻합니다.
atom이 변경되면, 구독되어 있는 컴포넌트로 변경사항이 전달된다.
MVVM에서는 atom이 Model의 데이터의 역할을 한다.
interface DeliveryMethodItemType {
id: number;
name: string;
delivery_price: number;
}
export const DeliveryMethodAtom = atom<DeliveryMethodItemType[]>({
key: "DeliveryMethodState",
default: [
...deliveryTypes
]
})
interface로 타입을 선언해주고, key로 다른 atom과 구분한다.
default에 atom의 기본값을 선언한다.
배열이나, 객체, 단일 값 모두 default로 선언 가능하다.
MVVM에서는 selector가 Model의 메서드 역할을 한다.
key : 다른 selector들과의 구분되는 id 역할을 한다.
get : 다른 atom들을 받아오거나, 있는 값으로 연산한 값을 return 한다.
// get만 존재하는 selector
export const PriceStatsSelector = selector({
key: "PriceStats/Read",
get: ({ get }) => {
const productList = get(ProductListAtom)
const allDeliveryPrice = get(SelectedMethodAtom).delivery_price
const checkedList = productList.filter(item => item.checked)
let allProductPrice = 0
let allAmount = 0
checkedList.map(({ product_price, current_count }) => {
allAmount += current_count
allProductPrice += product_price * current_count
return {allAmount, allProductPrice};
})
const allPrice = allProductPrice + allDeliveryPrice;
return {allProductPrice, allAmount, allDeliveryPrice, allPrice }
}
})
값을 읽는 코드는 이런식으로 작성한다.
const price = useRecoilValue(PriceStatsSelector)
const {allProductPrice, allAmount, allDeliveryPrice, allPrice } = price
또 다른 경우로 get, set모두가 있는 경우이다.
//get, set 모두가 있는 경우
export const AllCheckProductItemsSelector = selector({
key: "ProductList/Item/AllCheck",
get: ({ get }) => {
const productList = get(ProductListAtom)
if (productList) return false;
return get(ProductListAtom).every(item => item.checked)
},
set: ({ set }) => {
set(ProductListAtom, prevState => prevState.map(item => {
const currentItem = { ...item };
currentItem.checked = !currentItem.checked;
return currentItem;
})
)
}
})
set은 사용할 때 주의할 점이 있습니다.
- set을 할 때는 자기 자신의 selector를 set하려고 하면 loop가 돌기 때문에 자기자신을 set해선 안된다.
- selector는 read-only한 값을 가지기 때문에, writeable한 값을 가지고 set을 해야합니다.
- 위 코드로 설명하자면, prevState.map 에서 currentItem 이라고 아이템을 복사한 후에 수정하고, 수정된 아이템을 return 하는 이유는 prevState.map(item) 의 item은 readonly한 value 이므로, 한번 복사해서, writeable한 value로 만들어서 return 하는 것입니다.
이런 식으로 state(상태)를 atom으로, 각 topic의 CRUD를 selector로 구현하여서 Model을 구성합니다.
불러오는 코드는 이렇게 작성합니다.
//selector - get만 있는 경우
const price = useRecoilValue(PriceStatsSelector);
//selector 의 set만 불러오는 경우
const [, toggleChecked] = useRecoilState(CheckProductItemSelector);
//selector의 get, set 모두 필요한 경우
const [allChecked, allCheckedProductItem] = useRecoilState(
AllCheckProductItemsSelector
);
const [, increaseAmount] = useRecoilState(
IncreaseProductItemAmount
);
const [, decreaseAmount] = useRecoilState(
DecreaseProductItemAmount
);
const [, handleDeleteProductItem] =
useRecoilState(DeleteProductItem);
recoil은 상당히 새로 배우기도 쉽고, 바로바로 적용시키는데도 그렇게 많은 리스크가 있지않아서, 한번쯤은 해볼만한 라이브러리인 것 같다.
selector 의 set에서 writeable, readonly 에서 개념에도 애를 먹고, 그걸 토대로 적용하는 데에도 애를 좀 먹었지만 막상 MVVM의 model로 적용하고 나니 MVVM 패턴에도 쓸만한 상태관리인 것 같아 뿌듯했다.
setter만 가져올 때 useSetRecoliState를 쓰면 되겠군요!