이직을 준비하는 과정에서 새로운 상태관리를 적용해볼 필요가 있어, zustand를 사용했다.
이전 진행했던 프로젝트에서 상태관리 라이브러리 Jotai를 사용해본 경험이 있어서 그리 어렵지 않게 사용할 수 있었다.
(놀랍게도, 두 라이브러리 이름 모두 "상태" 이다. Zustand 제작에 Jotai 제작팀도 참여했다고 한다.)
Zustand 스토어를 만들때는create
함수를 이용해서 상태와 상태를 변경하는 액션 함수를 정의한다. 이렇게 정의된 상태와 액션은 훅으로 리턴해서 사용할 수 있다.
// src/store/useStore.ts
import create "zustand"
import { CartType } from "src/types"
export const useStore = create<CartType>(
(set) => {
// initial한 상태를 정의한다.
cart: [],
},
// 상태를 변경하는 액션을 정의한다.
addItem : (item) => ({
set((state) => ({
....state.cart,
cart: item,
}));
}),
removeItem: (id) => {
set((state) => ({
cart: state.cart.filter((item) => item.id !== id),
}));
},
})
);
먼저 create 함수를 통해서, initail한 상태를 정의 후에 이어 현재 상태를 인자로 넘겨주는 형식으로 다음 상태를 정의하는 액션을 정의할 수 있다.
스토어의 상태와 액션은 Subscribe
함수를 사용해서 상태의 모든 변화 혹은 일부를 구독할 수 있지만, 나는 React 컴포넌트에서 직접 사용할 수 있도록 훅으로 만들었다.
위 상태와 액션은 컴포넌트 단에서 아래와 같이 사용할 수 있다.
// src/components/cart.tsx
import React from 'react'
import {useStore} from 'src/store/useStore'
const CartItems = () => {
const { cart, addItem, removeItem } = useStore();
...
return(
<div>
{
cart && cart.map((item, index) => {
return (
<>
<ItemCard item={item} id={item.id}/>
<div>
<button onClick={() => addItem()}>담기 </button>
<button onClick={() => removeItem()}>빼기</button>
</div>
</>
)});
}
</div>
)
}
아주 간단하게 상태를 참조해서 카트의 아이템과 카트 아이템을 추가하거나 제거할 수 있는 컴포넌트를 렌더링해보았다.
위처럼 Zustand는 상태와 액션을 훅의 형태로 이용할 수 있기 때문에 Redux의 그것과는 달리 보일러플레이트 없이 간결하게 상태를 이용하거나 변경할 수 있다.
전역상태는 페이지가 refresh 될때 초기화된다. 하여, 상태를 기억하게 하기 위해서 갖가지 방법이 동원되는데 Zustand에서는 persist
미들웨어를 지원해주고 있어 상태를 쉽게 보존할 수 있다.
// src/store/useStore.ts
import create "zustand"
import { CartType } from "src/types"
import { persist } from 'zustand/middleware';
import { StateCreator } from 'zustand';
import { PersistOptions } from 'zustand/middleware';
type PersisType = (
config: StateCreator<CartType>,
options: PersistOptions<CartType>,
) => StateCreator<CartType>;
export const useStore = create<CartType>(
//상태와 액션을 persist 미들웨어로 감싸줍시다.
(persis as PersisType)(
(set) => {
// initial한 상태를 정의한다.
cart: [],
},
// 상태를 변경하는 액션을 정의한다.
addItem : (item) => ({
set((state) => ({
....state.cart,
cart: item,
}));
}),
removeItem: (id) => {
set((state) => ({
cart: state.cart.filter((item) => item.id !== id),
}));
},
}),
// persist 미들웨어로 Storage에 저장할 상태 명을 정의한다.
{name :'user_CartStore'},
),
);
페이지를 리프레쉬했을때 이렇게 로컬스토리지에 정의한 키 네임으로 상태들이 값으로 잘 저장되고 있는 것을 확인할 수 있다.
Next.js에서 해당 값이 저장된 것을 확인할 수 있겠지만, 상태 그대로를 컴포넌트에 참고해서 사용할 수 없다.
아마 페이지를 리프레쉬하면,
다음과 같은 에러메시지를 만나게 될 것이다.
Next.js는 서버사이드에서 페이지를 프리랜더링하게 되는데, 로컬스토리지에 있는 값은 클라이언트 사이드에서만 접근가능하다. 하여 undefined한 값들을 참조하게 되기 때문이다.
이를 해결하기 위해서 방법들을 찾던 중에 마침 비슷한 문제로 골머리를 앓던 분의 포스트를 찾게 되어, 문제를 해결할 수 있었다.
// src/hooks/useCart.ts
import { useEffect, useState } from 'react';
import { useStore } from 'src/store/useStore';
import { ProductInfoType } from 'src/type/index';
const useCart = () => {
const [userCart, setUserCart] = useState<ProductInfoType[]>([]);
const { cart } = useCartStore();
useEffect(() => {
const loacalCartData = localStorage.getItem('user_CartStore');
if (loacalCartData !== null) {
const restoreCartData = JSON.parse(loacalCartData);
if (restoreCartData.state.cart.length !== 0) {
setUserCart(restoreCartData.state.cart);
}
}
}, []);
// 해당 훅을 통해 카트 상태에 변경이 있었을 때 반영할 수 있따.
useEffect(() => {
setUserCart(cart);
}, [cart]);
return { userCart };
};
export default useCart;
원 글의 저자도 밝혔듯이 명확하게 문제를 해결한 것은 아니지만, 로컬스토리지에 저장된 값을 반영해서 업데이트한 상태를 참고할 수 있어 최선의 방법인 것 같다.
여전히 많은 프로젝트에서 리덕스와 리덕스 툴킷이 사용되고 있고, 그에 따라 비동기를 제거하기 위한 Redux thunk
나 Redux saga
패턴을 적용해서 액션 함수를 정의하고 있다.
물론, 위의 방법대로 프로젝트를 구성한다고 해서 문제가 있는 것은 아니다. 다만, jotai, recoil, Zustand같은 차세대 상태관리 라이브러리의 출현과 React-Query라는 강력한 서버 상태 관리 라이브러리가 자리잡은 지금 굳이 Redux를 고집할 필요는 없다고 생각한다.
코드란 결국 수많은 라인을 한 데 뭉쳐 하나의 프로덕트를 구성하는 요소이고, 이 요소는 코드를 작성한 작성자 뿐만 아니라 공동작업자들도 직관적으로 확인할 수 있어야 한다. 그런 점에서 코드의 흐름이 간결할 뿐만 아니라, 보일러 플레이트를 최소화 할 수 있는 Zustand 등 차세대 라이브러리 삼총사를 필히 경험해보라 추천하고 싶다.