[TIL-0511] Zustand์˜ persist

jinyยท2025๋…„ 5์›” 14์ผ

์บก์Šคํ†ค2

๋ชฉ๋ก ๋ณด๊ธฐ
6/22

๐ŸŒŸ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ชฉ๋ก ์ƒํƒœ ์กฐ๊ฑด


์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ชฉ๋ก ์ƒํƒœ๋Š” ๋‹ค์Œ ๋‘ ๊ฐ€์ง€ ์กฐ๊ฑด์„ ์ถฉ์กฑํ•ด์•ผ ํ•œ๋‹ค.

  1. ์œ„ ํŽ˜์ด์ง€(์žฅ์†Œ ํƒ์ƒ‰ ํŽ˜์ด์ง€)์—์„œ ์ƒˆ๋กœ๊ณ ์นจ์„ ํ•˜์—ฌ๋„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ชฉ๋ก์— ๋“ค์–ด์žˆ๋Š” ์š”์†Œ๊ฐ€ ์œ ์ง€๋˜์–ด์•ผ ํ•จ
  2. ์žฅ์†Œ ํƒ์ƒ‰ ํŽ˜์ด์ง€์—์„œ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ–ˆ๋‹ค๊ฐ€, ๋‹ค์‹œ ์žฅ์†Œ ํƒ์ƒ‰ ํŽ˜์ด์ง€๋กœ ๋Œ์•„์˜ค๋ฉด, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ชฉ๋ก์ด ๋ฆฌ์…‹๋˜์–ด์•ผ ํ•จ

โžก๏ธ ์ฆ‰, ์žฅ์†Œ ํƒ์ƒ‰ ํŽ˜์ด์ง€๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ์—๋งŒ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ชฉ๋ก์ด ๋ฆฌ์…‹๋˜์–ด์•ผ ํ•จ

์ฒซ ๋ฒˆ์งธ ์กฐ๊ฑด์„ ์ถฉ์กฑ์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ Zustand์˜ persist ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์˜€๋‹ค.


๐ŸŒŸ persist๋ž€?

  • ๋ธŒ๋ผ์šฐ์ €์— ์ƒํƒœ(state)๋ฅผ ์ €์žฅํ•ด์„œ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์œ ์ง€๋˜๋„๋ก ๋„์™€์ฃผ๋Š” ๊ธฐ๋Šฅ

  • React ์•ฑ์—์„œ๋Š” ์ƒํƒœ๋ฅผ useState๋‚˜ Zustand ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๊ด€๋ฆฌํ•จ โ†’ ๊ทธ๋Ÿฐ๋ฐ, ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์ด ์ƒํƒœ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋‘ ์ดˆ๊ธฐํ™”๋จ

    • ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•ด์„œ isLoggedIn: true์ธ ์ƒํƒœ๋ฅผ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์ƒํƒœ๋Š” ์‚ฌ๋ผ์ง€๊ณ  false๋กœ ๋Œ์•„๊ฐ€๋ฒ„๋ฆผ
    • ์ด๋Ÿด ๋•Œ persist๋ฅผ ์“ฐ๋ฉด, ์ƒํƒœ๋ฅผ ๋ธŒ๋ผ์šฐ์ €์˜ localStorage๋‚˜ sessionStorage์— ์ €์žฅํ•ด์„œ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ๋ณต๊ตฌ๋˜๊ฒŒ ํ•ด์คŒ

๐ŸŒŸ persist ์„ค์น˜ ๋ฐฉ๋ฒ•

  • persist๋Š” zustand ํŒจํ‚ค์ง€์— ๋‚ด์žฅ๋œ ๋ฏธ๋“ค์›จ์–ด์ž„
  • ๋”ฐ๋ผ์„œ zustand๋ฅผ ์„ค์น˜ํ•˜๋ฉด, persist๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
    npm install zustand

๐ŸŒŸ persist ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• (์˜ˆ์ œ)

๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ธฐ

// store/useAuthStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface AuthState {
  isLoggedIn: boolean;
  login: () => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      isLoggedIn: false,
      login: () => set({ isLoggedIn: true }),
      logout: () => set({ isLoggedIn: false }),
    }),
    {
      name: 'auth-storage', // localStorage์— ์ €์žฅ๋  key ์ด๋ฆ„
    }
  )
)

์‚ฌ์šฉ ์˜ˆ์‹œ (์ปดํฌ๋„ŒํŠธ)

import { useAuthStore } from './store/useAuthStore'

function App() {
  const { isLoggedIn, login, logout } = useAuthStore();
  
  return (
    <div>
      <h1>{isLoggedIn ? '๋กœ๊ทธ์ธ๋จ' : '๋กœ๊ทธ์•„์›ƒ๋จ'}</h1>
      <button onClick={login}>๋กœ๊ทธ์ธ</button>
      <button onClick={logout}>๋กœ๊ทธ์•„์›ƒ</button>
    </div>
  )
}
  • ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ isLoggedIn์„ true๋กœ ๋งŒ๋“  ๋’ค, ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์ƒํƒœ๊ฐ€ ์œ ์ง€๋จ

๐ŸŒŸ persist์˜ ์ €์žฅ ์œ„์น˜ ์ง€์ •ํ•˜๊ธฐ

  • ๊ธฐ๋ณธ์ ์œผ๋กœ persist๋Š” ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€(localStorage)์— ์ €์žฅํ•จ

  • ์ €์žฅ ์œ„์น˜๋ฅผ ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€(sessionStorage)๋กœ ๋ฐ”๊พธ๊ณ  ์‹ถ๋‹ค๋ฉด storage ์˜ต์…˜์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•ด์•ผ ํ•จ

    persist(
      (set) => ({ /* state */ }),
      {
        name: 'my-storage',
        storage: createJSONStorage(() => sessionStorage),
      }
    )
    • createJSONStorage() : Zustand์˜ ์ƒํƒœ๋ฅผ JSON ํ˜•ํƒœ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋‚˜ ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋ž˜ํผ ํ•จ์ˆ˜

    • ๋‚ ์งœ ๊ฐ์ฒด๋‚˜ ํŠน์ˆ˜ํ•œ ํฌ๋งท์ด ์žˆ๋‹ค๋ฉด replacer, reviver๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ข‹์Œ

      import { create } from 'zustand'
      import { persist, createJSONStorage } from 'zustand/middleware'
      
      interface DateState {
        someDate: Date;
        setDate: (date: Date) => void;
      }
      
      export const useDateStore = create<DateState>()(
        persist(
          (set) => ({
            someDate: new Date(),
            setDate: (date) => set({ someDate: date }),
          }),
          {
            name: 'date-store',
            storage: createJSONStorage(() => sessionStorage, {
              // ์ €์žฅํ•  ๋•Œ Date๋ฅผ ๊ฐ์ฒด๋กœ ๋ฐ”๊ฟˆ
              replacer: (key, value) => {
                if (key === 'someDate') return { type: 'date', value }
                return value
              },
              // ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ Date ๊ฐ์ฒด๋กœ ๋‹ค์‹œ ๋ณต์›
              reviver: (key, value) => {
                if (value && value.type === 'date') return new Date(value.value)
                return value;
              },
            }),
          }
        )
      )

      ๐Ÿง  ์ฝ”๋“œ ํ•ด์„

      1. replacer
        • ์ด ํ•จ์ˆ˜๋Š” ์–ธ์ œ ํ˜ธ์ถœ๋ ๊นŒ?
          • Zustand๊ฐ€ ์ƒํƒœ๋ฅผ ์ €์žฅ์†Œ์— ์ €์žฅํ•  ๋•Œ JSON.stringify()๋ฅผ ์”€
          • ์ด๋•Œ ๊ฐ key-value ์Œ๋งˆ๋‹ค ์ด replacer ํ•จ์ˆ˜๊ฐ€ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋จ
        • ๋ฌด์Šจ ์ผ์„ ํ• ๊นŒ?
          • key === 'someDate'์ธ ํ•ญ๋ชฉ, ์ฆ‰ ๋‚ ์งœ ๊ฐ’์„ ๋งŒ๋‚˜๋ฉด
          • ๋ฌธ์ž์—ด๋กœ ์ €์žฅํ•˜์ง€ ์•Š๊ณ , ํƒ€์ž… ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ๊ฐ์ฒด๋กœ ๊ฐ์‹ธ์„œ ์ €์žฅํ•จ
            "someDate": {
              "type": "date",
              "value": "2025-05-13T14:00:00.000Z",
            }
      2. reviver
        • ์ด ํ•จ์ˆ˜๋Š” ์–ธ์ œ ํ˜ธ์ถœ๋ ๊นŒ?
          • ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ JSON.parse()๋ฅผ ์”€
          • ๊ฐ key-value ์Œ๋งˆ๋‹ค ์ด reviver ํ•จ์ˆ˜๊ฐ€ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋จ
        • ๋ฌด์Šจ ์ผ์„ ํ• ๊นŒ?
          • ์ €์žฅ๋œ ๋ฌธ์ž์—ด์„ ๋ถˆ๋Ÿฌ์™”์„ ๋•Œ, value๊ฐ€ {type: 'date', value: '...'} ํ˜•ํƒœ๋ผ๋ฉด
          • ๊ทธ๊ฒƒ์„ ๋‹ค์‹œ new Date(value.value)๋กœ ๊ฐ์‹ธ์„œ ์ง„์งœ Date ๊ฐ์ฒด๋กœ ๋ณต์›ํ•จ

      โ“ replacer / reviver๊ฐ€ ํ•„์š”ํ•œ ์ด์œ 

      • ๋ธŒ๋ผ์šฐ์ € ์ €์žฅ์†Œ๋Š” ์˜ค์ง ๋ฌธ์ž์—ด๋งŒ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Œ
      • ๊ทธ๋ž˜์„œ Date ๊ฐ™์€ ๊ฐ์ฒด๋ฅผ ๊ทธ๋ƒฅ ์ €์žฅํ•˜๋ฉด "2025-05-13T13:00:00.000Z"์ฒ˜๋Ÿผ ๋ฌธ์ž์—ด๋กœ ๋ฐ”๋€œ
      • ๋ฌธ์ œ์ : ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ํƒ€์ž…์ด Date์˜€๋Š”์ง€ string์ด์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†์Œ โ†’ new Date()๋กœ ๋‹ค์‹œ ๋งŒ๋“ค์–ด์ค˜์•ผ ํ•จ
      • ๊ทธ๋ž˜์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ๋ถ„ํ•จ
        • replacer : ์ €์žฅํ•  ๋•Œ Date๋ฅผ {type: 'date', value: '...'} ํ˜•์‹์œผ๋กœ ๊ฐ์‹ธ์คŒ
        • reviver : ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ๊ทธ๊ฑธ ๋‹ค์‹œ new Date()๋กœ ๋ณต์›ํ•จ

๐ŸŒŸ ์ €์žฅ๋œ ์ƒํƒœ ์ง€์šฐ๋Š” ๋ฐฉ๋ฒ•

์ƒํƒœ๋ฅผ ๋ฆฌ์…‹ํ•˜๊ฑฐ๋‚˜ ์ €์žฅ์†Œ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” clearStorage๋ฅผ ์”€

import { useAuthStore } from './store/useAuthStore'
           
useAuthStore.persist.clearStorage();

๋˜๋Š” removeItem์„ ์‚ฌ์šฉํ•˜์—ฌ ์ œ๊ฑฐํ•ด๋„ ๋จ

sessionStorage.removeItem('date-store');

๐ŸŒŸ partialize๋ฅผ ์ด์šฉํ•ด์„œ ์ €์žฅํ•  ์ƒํƒœ ์„ ํƒํ•˜๊ธฐ

์ƒํƒœ๋ฅผ ์ผ๋ถ€๋งŒ ์ €์žฅํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” partialize ์˜ต์…˜์„ ์‚ฌ์šฉํ•จ

persist(
  (set, get) => ({
    username: '',
    token: '',
    isLoggedIn: false,
    setUsername: (name: string) => set({ username: name }),
    // ...
  }),
  {
    name: 'auth-storage',
    partialize: (state) => ({ token: state.token }), // token๋งŒ ์ €์žฅ
  }
)
  • persist๋Š” ์ƒํƒœ ์ „์ฒด ์ค‘์—์„œ token๋งŒ ์ €์žฅ์†Œ(๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋‚˜ ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€ ๋“ฑ)์— ์ €์žฅํ•จ
  • username๊ณผ isLoggedIn์€ ์ €์žฅ์†Œ์— ์ €์žฅ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ์ดˆ๊ธฐ๊ฐ’(''์™€ false)์œผ๋กœ ๋Œ์•„๊ฐ

๐ŸŒŸ ์ปค์Šคํ…€ serialize/deserialize

  • serialize : ์ €์žฅ๋˜๋Š” ๊ฐ’์„ ๋ฌธ์ž์—ด๋กœ ๋ฐ”๊พธ๋Š” ๊ณผ์ •
  • deserialize : ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ํ•ด์„ํ•˜๋Š” ๊ณผ์ •
  • JSON์ด ์•„๋‹Œ ๋‹ค๋ฅธ ํฌ๋งท์„ ์“ฐ๊ณ  ์‹ถ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜๋ฉด ๋จ
    persist(
      (set) => ({ /* ์ƒํƒœ*/ }),
      {
        name: 'custom-storage',
        serialize: (state) => btoa(JSON.stringify(state)), // Base64 ์ธ์ฝ”๋”ฉ
        deserialize: (str) => JSON.parse(atob(str)),
      }
    )

โœ๏ธ ๊ตฌํ˜„ ์˜ˆ์‹œ

// src/stores/favoriteList.store.ts

import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
           
type FavoriteItem = {
  placeName: string;
  placeImg: string;
};

interface FavoriteListState {
  favoriteList: FavoriteItem[];
  addFavoriteList: (placeName: string, placeImg: string) => void;
  deleteFavoriteList: (placeName: string, placeImg: string) => void;
  resetFavoriteList: () => void;
}
           
export const useFavoriteListStore = create<FavoriteListState>()(
  persist(
    (set, get) => ({
      favoriteList: [],
      addFavoriteList: (placeName: string, placeImg: string) => {
        const currentList = get().favoriteList;
        
        // ์ค‘๋ณต ์ถ”๊ฐ€ ๋ฐฉ์ง€
        const isDuplicate = currentList.some(
          (item) => item.placeName === placeName && item.placeImg === placeImg);
        if (isDuplicate) return;
        
        set({ favoriteList: [...currentList, { placeName, placeImg }]});
      },
      deleteFavoriteList: (placeName: string, placeImg: string) => {
        const currentList = get().favoriteList;
        
        set({
          favoriteList: currentList.filter((item) => item.placeName !== placeName && item.placeImg !== placeImg),
        });
      },
      resetFavoriteList: () => set({ favoriteList: [] }),
    }),
    {
      name: 'favorite-list',
      storage: createJSONStorate(() => sessionStorage),
    }
  )
)

์ •๋ณด ํƒ์ƒ‰ ํŽ˜์ด์ง€์—์„œ ๊ฒฝ๋ณต๊ถ์„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ณ ,

์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚ด์šฉ์ด ์ž˜ ๋“ค์–ด๊ฐ„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

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