Zustand๐Ÿป (with. zod)

seunghye joยท2023๋…„ 9์›” 8์ผ
0

react library

๋ชฉ๋ก ๋ณด๊ธฐ
3/3
post-thumbnail

์„ค์น˜

yarn add zustand

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

schema์™€ type ์ƒ์„ฑ

// 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>;

store ์ƒ์„ฑ

๋ฐฉ๋ฒ• 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();

state์˜ ์‚ฌ์šฉ

  1. ์ž˜๋ชป๋œ ์‚ฌ์šฉ

    /**
    * โŒ ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง์ด ์ผ์–ด๋‚  ์ˆ˜ ์žˆ์Œ 
    * => useTodoStore()๋Š” ์‹ค์ œ๋กœ ์ „์ฒด state๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์—
    */
    
    const {todos, addTodo, checkTodo, removeTodo} = useTodoStore()
  2. shallow ์‚ฌ์šฉ - ์—…๋ฐ์ดํŠธ ์ „ํ›„ ๊ฐ’์„ ๋น„๊ตํ•˜์—ฌ ๊ฐ’์ด ๋ณ€ํ•˜์˜€์„ ๋•Œ๋งŒ ๋ฆฌ๋ Œ๋”๋ง์ด ์ผ์–ด๋‚˜๋„๋ก ํ•ด

    const { todos, addTodo } = useTodoStore(
        (state) => ({
          todos: state.todos,
          addTodo: state.actions.addTodo,
        }),
        shallow
      );
  3. ๊ฐ๊ฐ ํ˜ธ์ถœ - ์‚ฌ์šฉํ•  ๋•Œ ๋งˆ๋‹ค (state) => state.todos ์™€ ๊ฐ™์€ selector๋ฅผ ์ž…๋ ฅํ•ด์ค˜์•ผ ํ•จ

    // store ์ƒ์„ฑ์—์„œ actions๋ฅผ ๋ถ„๋ฆฌํ•œ ๊ฒฝ์šฐ
    const todos = useTodoStore((state) => state.todos);
    const {addTodo, checkTodo} = useTodoStore((state) => state.actions);
  4. 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()
    (...)
    ```

Middleware

persist์‚ฌ์šฉ

๋ฐ์ดํ„ฐ๋ฅผ 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 })}
    )
);

persist options

  • name : storage์— ์ €์žฅ ๋  ๋ฐ์ดํ„ฐ์˜ ์ด๋ฆ„์„ ์ง€์ •. ๊ณ ์œ ํ•œ ์ด๋ฆ„์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ.
  • partialize :
  • storage์— ์ €์žฅ ๋  ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Œ.
  • ๊ฐ์ฒด์˜ key๊ฐ’์€ ๋ฐ˜๋“œ์‹œ state์˜ key๊ฐ’๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•จ
    ์˜ˆ๋ฅผ ๋“ค์–ด ์•„๋ž˜์™€ ๊ฐ™์ด { todos: state.todos[0] } ๋ฅผ returnํ•ด์ฃผ๋ฉด todos ๋ฐฐ์—ด์˜ ์ฒซ๋ฒˆ์งธ ๊ฐ์ฒด๋งŒ storage์— ์ €์žฅ์ด ๋จ.
    ```tsx
    { name: "todo-storage", partialize: (state) => ({ todos: state.todos[0] })}
    ```

immer ์‚ฌ์šฉ

๊ฐ์ฒด,๋ฐฐ์—ด ๋ฐ์ดํ„ฐ์˜ ๋ถˆ๋ณ€์„ฑ ๊ด€๋ฆฌ

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 },
})));

devtools ์‚ฌ์šฉ

๊ฐœ๋ฐœ์ž๋„๊ตฌ.

chromeํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ redux devtools ์„ค์น˜ ํ›„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

import { devtools } from "zustand/middleware";

export const useTodoStore = create<TodoState>()(
  devtools(
		(set) => {...}
  )
);

๊ฐœ๋ฐœ์ž๋„๊ตฌ์˜ Reduxํƒญ์—์„œ ํ™•์ธ๊ฐ€๋Šฅ

Untitled

๐Ÿ’ก **๊ถŒ์žฅ middleware ์ค‘์ฒฉ์ˆœ์„œ :** `immer > devtools > persist`

๊ธฐํƒ€ ์„ค์ •

useStore๋กœ hydration์—๋Ÿฌ ์˜ˆ๋ฐฉ

  1. ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ๋ณต๋ถ™ํ•˜์—ฌ 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;
  2. 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์‚ฌ์šฉ์œผ๋กœ ์—๋Ÿฌ ๋ฐฉ์ง€.

React-query์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ

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),
  })
}
profile
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž ์„ฑ์žฅ์ผ๊ธฐ ๐Ÿ’ญ

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2024๋…„ 8์›” 1์ผ

state ์‚ฌ์šฉ๋ถ€๋ถ„ ์ž˜ ๋ชฐ๋ž๋˜ ๋ถ€๋ถ„์ธ๋ฐ ๊ธ€ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค :)
์ค‘์ฒฉ ์ˆœ์„œ์— ๋Œ€ํ•ด ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋Š”๋ฐ, ํ˜น์‹œ devtools๋ฅผ ๊ฐ€์žฅ ์ฒ˜์Œ์— ๋‘๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•˜์ง€ ์•Š์„๊นŒ์š”? (https://docs.pmnd.rs/zustand/guides/typescript#using-middlewares)

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ