Zustand
이전 프로젝트에서는 Jotai를 사용했는데, 이번 프로젝트는 나 빼고 다 Zustand를 사용하는 까닭에 내가 팀원들에게 맞춰야지 하고 있다. 공식 문서 읽으면서 필요하다 싶은 부분들을 정리해보았다.
Provider의 부재
Jotai는 Provider와 Store를 통해 값을 제한할 수 있는데, Zustand에서는 그런 기능은 없는 듯하다. 이게 장점인지 단점인지는 모르겠다. 일반적으로 상태관리 도구는 전역 상태관리를 위해 사용하는 경우가 많으므로 크게 문제가 되지는 않을 것 같다.
Store
useReducer 훅과는 비슷한 것 같으면서도 다르다. 여기서는 store를 생성할 때 초기값과 액션들을 미리 지정하는 듯하다. 일전에 누군가 내게 'Zustand의 구조가 객체지향적이라 Jotai보다 Zustand가 내 취향에 맞을 것'이라 했는데, 그 이유를 알 것 같다. store는 상태와 동작을 포함한다.
import { create } from "zustand";
type State = { bears: number };
type Action = {
increasePopulation: () => void;
addPopulation: (number: number) => void;
removeAllBears: () => void;
};
const useBears = create<State & Action>((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
addPopulation: (number) => set((state) => ({ bears: state.bears + number })),
removeAllBears: () => set({ bears: 0 }),
}));
export default useBears;
Zustand에서는 set 함수를 통해 상태를 변경한다. 여기에는 크게 두 방법이 있는데 1) 콜백 함수를 사용해 기존의 값을 반영하여 새로운 값으로 갱신하는 것과 2) 기존의 값을 무시하고 언제든 정해진 값으로 변경하는 방법이 그것이다.
// 타입 에러 발생
removeAllChics: () => set({ chics: 0 }),
존재하지 않는 상태를 변경하려는 시도는 타입스크립트에 의해 저지될 것이니 굳이 시도해보지 않는 것이 좋겠다.
갖다 쓰기
앞서 만든 store는 구조 분해 할당하는 것만으로도 사용할 수 있다. 테스트 용도로 간단히 아래의 코드를 짜보았는데, 정말이지 마법처럼 잘 작동한다. 고도로 발달한 상태관리는 마법과 구분할 수 없다나 뭐라나.
export default function Home() {
const { bears, addPopulation, removeAllBears } = useBears();
const [value, setValue] = useState<number>(0);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(Number(e.target.value));
};
return (
<>
<div>{bears}</div>
<input type="number" value={value} onChange={handleChange} />
<button type="button" onClick={() => addPopulation(value)}>
add bear
</button>
<button type="button" onClick={removeAllBears}>
zero bear
</button>
</>
);
}
만약 이 store의 상태와 동작의 이름이 겹치는 경우가 있다면 ─ 물론 구조 분해 할당 과정에서 변수의 이름을 바꿔주는 것도 가능하지만 ─ 아래와 같이 하나씩 호출하는 것도 가독성 부분에서 나쁘지 않을 것 같다.
// 곰 상태
const bears = useBears((state) => state.bears);
const addBearsPopulation = useBears((state) => state.addPopulation);
// 닭 상태
const chics = useChics((state) => state.chics);
const addChicsPopulation = useChics((state) => state.addPopulation);
아니면 useInputController 만들 때처럼 객체를 구조분해하지 않고 통채로 갖다 쓰는 방법도 있겠다.
// 곰 상태
const bears = useBears();
bears.bears
bears.addPopulation
// 닭 상태
const chics = useChics();
chics.chics
chics.addPopulation
Reducer
공식 문서에서 이르기를 If you can't live without Redux-like reducers, 즉 당신이 리덕스처럼 리듀서가 없으면 뒈질 것 같은 사람이라면 아래와 같은 방법으로 reducer와 dispatch 구조를 짤 수 있다. 그러나 솔찍한 심정으로는 왜 이렇게까지 해야 하는걸까 세상에는 정말 변태가 많아… 하는 생각 뿐이다.
const types = { increase: 'INCREASE', decrease: 'DECREASE' }
const reducer = (state, { type, by = 1 }) => {
switch (type) {
case types.increase:
return { grumpiness: state.grumpiness + by }
case types.decrease:
return { grumpiness: state.grumpiness - by }
}
}
const useGrumpyStore = create((set) => ({
grumpiness: 0,
dispatch: (args) => set((state) => reducer(state, args)),
}))
const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })
Persist
영단어 persist는 '끈질기게 (없어지지 않고) 계속되다'라는 의미를 가지고 있다. Zustand에서 persist는 zustand store의 상태를 브라우저의 로컬 스토리지나 세션 스토리지에 저장하는 역할을 한다. 이를 통해 브라우저를 다시 시작하거나 탭을 닫았다가 다시 열어도 상태가 (끈질기게 계속) 유지되게 한다.
타입스크립트를 쓰면 타입은 create가 아니라 persist 쪽에 달아주어야 한다. 이 부분은 햇갈리지 않게 주의해야겠다.
export const useTokenStore = create(
persist<Tokens & TokenSetter>(
(set) => ({
accessToken: "",
refreshToken: "",
setAccessToken: (token) => set({ accessToken: token }),
setRefreshToken: (token) => set({ refreshToken: token }),
}),
{
name: "token", // name of the item in the storage (must be unique)
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
}
)
);
컴포넌트에서 갖다 쓸 때는 일반적인 store 쓰듯 하면 된다.
export default function Home() {
const { accessToken, refreshToken, setAccessToken, setRefreshToken } =
useTokenStore();
return (
<>
<div>
{accessToken} & {refreshToken}
</div>
<button type="button" onClick={() => setAccessToken("death")}>
add accessToken
</button>
<button type="button" onClick={() => setRefreshToken("tax")}>
add refreshToken
</button>
</>
);
}
스토리지에는 아래와 같은 방식으로 저장된다. 테스트해보니 따로 뭘 해주지 않아도 이미 스토리지에 있는 값은 초기 로딩 과정에서 Zustand가 알아서 집어오는 듯하다.
// sessionStorage
{"state":{"accessToken":"death","refreshToken":"tax"},"version":0}
Jotai는 로컬 스토리지 밖에 못 쓰는 듯한데, Zustand는 세션 스토리지까지 쓸 수 있으니 필요에 맞게 사용하면 되겠다. 다만, 아무리 생각해도 값을 가져오고 사용하고 하는 등의 과정은 Jotai 쪽이 좀 더 간단하지 않은가 생각되어진다.
블로그의 정보
Ayden's journal
Beard Weard Ayden