yarn add zustand
// schema.ts
import { z } from "zod";
// state ์คํค๋ง ์ ์
export const TodoSchema = z.object({
id: z.number(),
content: z.string(),
isDone: z.boolean(),
});
// ์ ์ฒด ์คํ ์ด์ ๋ํ ์คํค๋ง ์ ์ (state + actions)
export const TodoStoreSchema = z.object({
todos: TodoSchema.array(),
actions: z.object({
addTodo: z.function().args(z.string()),
checkTodo: z.function().args(z.number()),
removeTodo: z.function().args(z.number()),
}),
});
// types.ts
import { z } from "zod";
import { TodoStoreSchema } from "./schema";
export type TodoStoreState = z.infer<typeof TodoStoreSchema>;
๋ฐฉ๋ฒ 1
// useTodoStore.ts
import { TodoStoreState } from "./types";
export const useTodoStore = create<TodoStoreState>((set) => ({
todos: [],
addTodo: (content) => set((state) => {...}),
checkTodo: (todoId) => set((state) => {...}),
removeTodo: (todoId) => set((state) => {...}),
}));
๋ฐฉ๋ฒ2 - ์ฐธ๊ณ ๐
// useTodoStore.ts
import { TodoStoreState } from "./types";
export const useTodoStore = create<TodoStoreState>((set) => ({
todos: [],
actions: {
addTodo: (content) => set((state) => {...}),
checkTodo: (todoId) => set((state) => {...}),
removeTodo: (todoId) => set((state) => {...}),
}
}));
โ ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง์ ๋ง๊ธฐ ์ํด ๊ฐ state๋ฅผ ๋ฐ๋ก ํธ์ถํด์ผํ๋ store์ ํน์ฑ ์ ๋ฐฉ๋ฒ 1
์ ๊ฒฝ์ฐ ์์ฑ์ ๊ฐ๋จํ์ง๋ง ํธ์ถ ์ ์ฝ๋๊ฐ ๊ธธ์ด์ง ์ ์์
const todos = useTodos();
const addTodo = useAddTodo();
const checkTodo = useCheckTodo();
const removeTodo = useRemoveTodo();
โ state๋ ๋ฐ๋์ ๋ฐ๋ก ํธ์ถํ๋ ๊ฒ์ด ์ข์ง๋ง actions์ ๊ฒฝ์ฐ ๊ฐ์ด ๊ณ ์ ๋์ด์๊ธฐ ๋๋ฌธ์ ๋ฆฌ๋ ๋๋ง์ ์ํฅ์ ์ฃผ์ง ์์ผ๋ฏ๋ก ๋ฐฉ๋ฒ2
์ ๊ฐ์ด ๋ฌถ์ด์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ฌ์ฉํ ๋ ๊ฐํธํจ
const todos = useTodos();
const { addTodo, checkTodo, removeTodo } = useTodoActions();
์๋ชป๋ ์ฌ์ฉ
/**
* โ ๋ถํ์ํ ๋ ๋๋ง์ด ์ผ์ด๋ ์ ์์
* => useTodoStore()๋ ์ค์ ๋ก ์ ์ฒด state๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ๋๋ฌธ์
*/
const {todos, addTodo, checkTodo, removeTodo} = useTodoStore()
shallow ์ฌ์ฉ - ์ ๋ฐ์ดํธ ์ ํ ๊ฐ์ ๋น๊ตํ์ฌ ๊ฐ์ด ๋ณํ์์ ๋๋ง ๋ฆฌ๋ ๋๋ง์ด ์ผ์ด๋๋๋ก ํด
const { todos, addTodo } = useTodoStore(
(state) => ({
todos: state.todos,
addTodo: state.actions.addTodo,
}),
shallow
);
๊ฐ๊ฐ ํธ์ถ - ์ฌ์ฉํ ๋ ๋ง๋ค (state) => state.todos
์ ๊ฐ์ selector๋ฅผ ์
๋ ฅํด์ค์ผ ํจ
// store ์์ฑ์์ actions๋ฅผ ๋ถ๋ฆฌํ ๊ฒฝ์ฐ
const todos = useTodoStore((state) => state.todos);
const {addTodo, checkTodo} = useTodoStore((state) => state.actions);
custom hook์ exportํ์ฌ ์ฌ์ฉ - ์ถ์ฒ
(์ฌ์ฉ์ ์ผ์ผ์ด selector๋ฅผ ์
๋ ฅํ์ง ์์๋ ๋์ด์ ํธํจ)
```tsx
// useTodoStore.ts
export const useTodos = () => useTodoStore(state => state.todos);
export const useTodoActions = () => useTodoStore(state => state.actions);
```
```tsx
// page.tsx
(...)
const numA = useNumA()
const { increaseA } = useCounterActions()
(...)
```
๋ฐ์ดํฐ๋ฅผ storage์ ์๋์ผ๋ก ์ ์ฅ
partialize
์์ฑ์ ์ฌ์ฉํ์ฌ storage์ state๋ง ๋ด๊ธฐ๋๋ก ์ค์
(โ zustand์์ storage์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ ํจ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ธํ๊ณ ์ ์ฅ.
partialize
๋ฏธ์ ์ฉ ์ actions ๊ฐ์ฒด ๋ด์ ํจ์๋ค์ ์ ์ํ๋ฉด storage๋ด์ actions๊ฐ ๋น ๊ฐ์ฒด๋ก ์ ์ฅ๋์ด ํจ์ ๊ฐ์ฒด๋ค์ด ์ง์์ง)
import { persist } from "zustand/middleware";
export const useTodoStore = create<TodoState>()(
persist(
(set) => {...},
{ name: "todo-storage", partialize: (state) => ({ todos: state.todos })}
)
);
name
: storage์ ์ ์ฅ ๋ ๋ฐ์ดํฐ์ ์ด๋ฆ์ ์ง์ . ๊ณ ์ ํ ์ด๋ฆ์ผ๋ก ์ค์ ํด์ผ ํจ.partialize
: { todos: state.todos[0] }
๋ฅผ returnํด์ฃผ๋ฉด todos ๋ฐฐ์ด์ ์ฒซ๋ฒ์งธ ๊ฐ์ฒด๋ง storage์ ์ ์ฅ์ด ๋จ.```tsx
{ name: "todo-storage", partialize: (state) => ({ todos: state.todos[0] })}
```
๊ฐ์ฒด,๋ฐฐ์ด ๋ฐ์ดํฐ์ ๋ถ๋ณ์ฑ ๊ด๋ฆฌ
yarn add immer
- ์ค์น ํ์
import { immer } from "zustand/middleware/immer";
// immer๊ฐ persist๋ฅผ ๊ฐ์ผ๋ค
export const useTodoStore = create<TodoState>()(
immer(
persist(
(set) => {...},
{ name: "todo-storage" }
)
)
);
// immer ์ ์ฉ ์
export const useCounterStore = create<CounterState>()(immer((set) => ({
count: 0,
increase: () => set((state) => ({ ...state, count: state.count + 1 })),
})));
// immer ์ ์ฉ ํ
export const useCounterStore = create<CounterState>()(immer((set) => ({
count: 0,
increase: () => set((state) => { state.count += 1 },
})));
๊ฐ๋ฐ์๋๊ตฌ.
chromeํ์ฅ ํ๋ก๊ทธ๋จ redux devtools ์ค์น ํ ์ฌ์ฉ ๊ฐ๋ฅ
import { devtools } from "zustand/middleware";
export const useTodoStore = create<TodoState>()(
devtools(
(set) => {...}
)
);
๊ฐ๋ฐ์๋๊ตฌ์ Reduxํญ์์ ํ์ธ๊ฐ๋ฅ
์๋ ์ฝ๋๋ฅผ ๋ณต๋ถํ์ฌ useStore.ts
ํ์ผ ์์ฑ
import { useState, useEffect } from "react";
const useStore = <T, F>(
store: (callback: (state: T) => unknown) => unknown,
callback: (state: T) => F
) => {
const result = store(callback) as F;
const [data, setData] = useState<F>();
useEffect(() => {
setData(result);
}, [result]);
return data;
};
export default useStore;
state selector hook ์์ฑ์ useStore์ store์ selector ์ ๋ฌ
// state์ฌ์ฉ์ useStore์ store์ selector ์ ๋ฌ
export const useTodos = () => useStore(useTodoStore, (state) => state.todos);
// actions๋ useStore๋ฅผ ์ฌ์ฉํ์ง ์์
export const useTodoActions = () => useTodoStore((state) => state.actions);
๐ก * zustand๋ ๋ธ๋ผ์ฐ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉ.
* zustand์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ํ๋ฉด์ ๊ทธ๋ฆฌ๊ฒ ๋๋ฉด
next์ ์๋ฒ์ธก ๋ ๋๋ง๊ณผ ํด๋ผ์ด์ธํธ ๋ ๋๋ง์ด ๋ฌ๋ผ ์๋ฌ๊ฐ ๋ฐ์.
* zustand๊ฐ ์ปดํฌ๋ํธ๋ฅผ ๋ณ๊ฒฝํ๊ธฐ ์ ์ ์ ์ ๊ธฐ๋ค๋ฆฌ๋๋ก ํ๋ useStore hook์ฌ์ฉ์ผ๋ก ์๋ฌ ๋ฐฉ์ง.
const useFilterStore = create((set) => ({
applied: [],
actions: {
addFilter: (filter) =>
set((state) => ({ applied: [...state.applied, filter] })),
},
}))
export const useAppliedFilters = () =>
useFilterStore((state) => state.applied)
export const useFiltersActions = () =>
useFilterStore((state) => state.actions)
// ๐ zustand store๋ฅผ query์ ๊ฒฐํฉ
export const useFilteredTodos = () => {
const filters = useAppliedFilters()
return useQuery({
queryKey: ['todos', filters],
queryFn: () => getTodos(filters),
})
}
state ์ฌ์ฉ๋ถ๋ถ ์ ๋ชฐ๋๋ ๋ถ๋ถ์ธ๋ฐ ๊ธ ๊ฐ์ฌํฉ๋๋ค :)
์ค์ฒฉ ์์์ ๋ํด ๊ถ๊ธํ ์ ์ด ์๋๋ฐ, ํน์ devtools๋ฅผ ๊ฐ์ฅ ์ฒ์์ ๋๋ ๊ฒ์ด ๋ ์ ์ ํ์ง ์์๊น์? (https://docs.pmnd.rs/zustand/guides/typescript#using-middlewares)