
처음 콜라콜라 벤딩머신을 순수 자바스크립트로만 만들었는데, 미뤄왔던 리팩토링을 이번에 진행하였다.
리팩토링이라고 하면 주제나 기능 업그레이드, 기술적 성장 등 여러가지가 있지만, 이번 나의 리팩토링 주제는 리코일 요리조리 활용 으로 정했다 🍳👩🏻🍳
사실 React를 계속 사용하다 보면, 예전 JavaScript와 jQuery로 기능을 구현하던 때와 많이 비교가 된다.
어느 순간 자바스크립트 클래스 문법... this... 등 기본 개념들이 희미해지며, 리액트 함수 컴포넌트... 화살표 함수에 익숙해져 있는 나 자신을 보며 예전 콜라콜라 벤딩머신 코드를 통해 참회하는 시간을 가졌다 🙃🫠🥴

오케이 반성 완료했고, 리액트 타입스크립트로 코드를 변환하면서 느낀 것은...
그리하여 이 2가지로 인해 이번 리팩토링도 재밌었다. 더 빨리 진행할 걸... ㅎㅎ
마이 컴퍼니 프로젝트할 때 리코일을 확실하게 활용했다는 생각이 들지 않았는데, 벤딩머신 리팩토링할 때 리코일의 이점을 정확히 깨달았다.
특히 선택자 활용해서 상태를 변환하고 계산하는 부분에 눈을 뜬 기분이다 😁
Piece of Cake 벤딩머신은 API를 호출하거나, 데이터 통신을 사용하는 부분이 없기 때문에 생각보다 간단했다. 그래서 더욱 리코일을 사용한 전역 상태 관리에 집중할 수 있었던 것도 같다.
| 초기 화면 | 메뉴얼 모달 |
|---|---|
| 소지금 충전 | 입금액 충전 |
|---|---|
| 아이템 획득 | 장바구니 초기화 |
|---|---|
| 거스름돈 반환 | 404 에러 |
|---|---|
총금액을 계산할 때, calculateTotalPrice 함수를 컴포넌트 내에서 직접 호출하고, useRecoilState 훅을 통해 상태 저장소의 totalCartItemState 상태를 가져와서 사용하고 있다.
하지만 이는 컴포넌트 내에 로직이 섞여 복잡성이 증가하고, 상태 업데이트와 같은 다른 부분에 영향을 미칠 수 있다!
// TotalCart.tsx
const TotalCart = () => {
const [cartItem, setCartItem] = useRecoilState(totalCartItemState);
const calculateTotalPrice = (): number => {
return cartItem.reduce(
(total: number, item: SelectedCakeItem) => total + item.cost * item.quantity,
0
);
};
}
...
return (
<>
<S.TotalPrice>총금액 : {calculateTotalPrice().toLocaleString()}원</S.TotalPrice>
</>
)
}
따라서 총금액 계산하는 부분을 셀렉터를 사용해 상태 관리 로직에서 분리시켜 컴포넌트는 UI에만 집중하고, 선택자는 데이터의 계산과 처리에 집중할 수 있도록 했다.
// selector.ts
export const totalPriceSelector = selector<number>({
key: 'totalPriceSelector',
get: ({ get }) => {
const cartItems: SelectedCakeItem[] = get(totalCartItemState);
return cartItems.reduce(
(total: number, item: SelectedCakeItem): number => total + item.cost * item.quantity,
0
);
},
});
useRecoilValue 훅을 통해 셀렉터에서 만든 totalPriceSelector를 가져와서 totalPrice 변수에 저장해서 사용하면 코드가 간결해지고, 유지보수에 용이해진다!
// TotalCart.tsx
const TotalCart = () => {
const [cartItem, setCartItem] = useRecoilState(totalCartItemState);
const totalPrice = useRecoilValue(totalPriceSelector);
...
return (
<>
<S.TotalPrice>총금액 : {totalPrice.toLocaleString()}원</S.TotalPrice>
</>
)
}
장바구니 초기화 버튼을 클릭할 때, cartItemRefresh 함수를 통해 장바구니에 있는 아이템들을 초기화시키며 useRecoilState 훅을 통해 상태 저장소의 totalCartItemState 상태를 가져와서setCartItem을 업데이트하고 있다.
초기화가 되면서 총금액이 다시 잔액과 합산되어 계산되어야 하는데, 장바구니만 초기화가 이루어지고 있다!
// TotalCart.tsx
const TotalCart = () => {
const [cartItem, setCartItem] = useRecoilState(totalCartItemState);
const cartItemRefresh = (): void => {
setCartItem([]);
};
...
return (
<>
<S.RefreshBtn onClick={cartItemRefresh} />
</>
)
}
따라서 장바구니를 초기화한 뒤, 총금액과 소지금을 더해주는 값을 계산해야하기 때문에, 셀렉터에서 총금액 + 잔액을 계산하는 선택자 totalPriceAmountSelector를 만들었다.
// selector.ts
export const totalPriceAmountSelector = selector<number>({
key: 'totalPriceAmountSelector',
get: ({ get }) => {
const cartItems = get(totalPriceSelector);
const balance = get(balanceState);
return cartItems + balance;
},
});
그리고 cartItemRefresh 함수에서 useSetRecoilState 훅을 통해 상태 저장소에서 잔액 관련 balanceState 상태를 가져온다.
그 다음, useRecoilValue 훅을 통해 totalPriceAmountSelector 선택자를 totalPriceBalance 변수에 저장하고, setBalance를 통해 해당 변수를 업데이트 하면 된다!
이렇게 하면 장바구니를 초기화하는 동시에 잔액도 함께 업데이트된다! 😉
// TotalCart.tsx
const TotalCart = () => {
const [cartItem, setCartItem] = useRecoilState(totalCartItemState);
const totalPriceBalance = useRecoilValue(totalPriceAmountSelector);
const setBalance = useSetRecoilState(balanceState);
const cartItemRefresh = (): void => {
// 1. 새로고침 버튼을 누르면 장바구니 초기화
// 2. 잔액 === 총금액 + 잔액
setCartItem([]);
setBalance(totalPriceBalance);
};
...
return (
<>
<S.RefreshBtn onClick={cartItemRefresh} />
</>
)
}
import * as S from './TotalCart.style';
import Wallet from '../Wallet/Wallet';
import ManualModal from '../Modal/ManualModal';
import useModal from '../../hooks/useModal';
import { SelectedCakeItem, totalCartItemState } from '../../state/atoms/atoms';
import { useRecoilState } from 'recoil';
const TotalCart = () => {
const { isModalOpen, openModal, closeModal } = useModal();
const [cartItem, setCartItem] = useRecoilState(totalCartItemState);
const calculateTotalPrice = (): number => {
return cartItem.reduce(
(total: number, item: SelectedCakeItem) => total + item.cost * item.quantity,
0
);
};
const cartItemRefresh = (): void => {
setCartItem([]);
};
return (
<>
<Wallet />
<S.Wrapper>
<S.H2>장바구니</S.H2>
<S.GetCartUl>
{cartItem.map((item: SelectedCakeItem) => (
<S.GetCartLi>
<S.GetCartImg src={item.img} alt={item.name} />
<S.GetCartName>{item.name}</S.GetCartName>
<S.GetCartStrong>{item.quantity}</S.GetCartStrong>
</S.GetCartLi>
))}
</S.GetCartUl>
<S.BottomBox>
<S.ButtonBox>
<S.ManualBtn onClick={openModal}>설명서</S.ManualBtn>
<S.RefreshBtn onClick={cartItemRefresh} />
</S.ButtonBox>
<S.TotalPrice>총금액 : {calculateTotalPrice().toLocaleString()}원</S.TotalPrice>
</S.BottomBox>
</S.Wrapper>
{isModalOpen && <ManualModal onClose={closeModal} />}
</>
);
};
export default TotalCart;
// selector.ts
import { selector } from 'recoil';
import { balanceState, SelectedCakeItem, totalCartItemState } from '../atoms/atoms';
// 총금액 계산
export const totalPriceSelector = selector<number>({
key: 'totalPriceSelector',
get: ({ get }) => {
const cartItems: SelectedCakeItem[] = get(totalCartItemState);
return cartItems.reduce(
(total: number, item: SelectedCakeItem): number => total + item.cost * item.quantity,
0
);
},
});
// 총금액 + 잔액
export const totalPriceAmountSelector = selector<number>({
key: 'totalPriceAmountSelector',
get: ({ get }) => {
const cartItems = get(totalPriceSelector);
const balance = get(balanceState);
return cartItems + balance;
},
});
import * as S from './TotalCart.style';
import Wallet from '../Wallet/Wallet';
import ManualModal from '../Modal/ManualModal';
import useModal from '../../hooks/useModal';
import { balanceState, SelectedCakeItem, totalCartItemState } from '../../state/atoms/atoms';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { totalPriceAmountSelector, totalPriceSelector } from '../../state/selectors/selector';
const TotalCart = () => {
const { isModalOpen, openModal, closeModal } = useModal();
const [cartItem, setCartItem] = useRecoilState(totalCartItemState);
const totalPrice = useRecoilValue(totalPriceSelector);
const totalPriceBalance = useRecoilValue(totalPriceAmountSelector);
const setBalance = useSetRecoilState(balanceState);
const cartItemRefresh = (): void => {
// 1. 새로고침 버튼을 누르면 장바구니 초기화
// 2. 잔액 === 총금액 + 잔액
setCartItem([]);
setBalance(totalPriceBalance);
};
return (
<>
<Wallet />
<S.Wrapper>
<S.H2>장바구니</S.H2>
<S.GetCartUl>
{cartItem.map((item: SelectedCakeItem) => (
<S.GetCartLi>
<S.GetCartImg src={item.img} alt={item.name} />
<S.GetCartName>{item.name}</S.GetCartName>
<S.GetCartStrong>{item.quantity}</S.GetCartStrong>
</S.GetCartLi>
))}
</S.GetCartUl>
<S.BottomBox>
<S.ButtonBox>
<S.ManualBtn onClick={openModal}>설명서</S.ManualBtn>
<S.RefreshBtn onClick={cartItemRefresh} />
</S.ButtonBox>
<S.TotalPrice>총금액 : {totalPrice.toLocaleString()}원</S.TotalPrice>
</S.BottomBox>
</S.Wrapper>
{isModalOpen && <ManualModal onClose={closeModal} />}
</>
);
};
export default TotalCart;