Zustand + persist 미들웨어로 상태 관리

개요

Zustand의 persist 미들웨어를 사용하여 상태를 AsyncStorage에 자동으로 저장/복원하는 방법을 정리합니다.


1. persist란?

Zustand의 미들웨어로, 상태를 자동으로 저장하고 복원해주는 기능입니다.

┌─────────────────────────────────────────────┐
│                Zustand Store                │
│                                             │
│ 상태 변경 → persist가 자동으로 AsyncStorage에 저장 │
│                                             │
│앱 재시작 → persist가 자동으로 AsyncStorage에서 복원 │
└─────────────────────────────────────────────┘

2. 설치

npm install zustand

Zustand에 persist 미들웨어가 내장되어 있어 별도 설치 불필요


3. 기본 사용법

persist 없이 (수동)

// 저장할 때마다 직접 AsyncStorage 호출
const setToken = async (token: string) => {
    set({ accessToken: token });
    await AsyncStorage.setItem("token", token);  // 수동
};

// 앱 시작 시 직접 복원
useEffect(() => {
    const loadToken = async () => {
        const token = await AsyncStorage.getItem("token");  // 수동
        if (token) setToken(token);
    };
    loadToken();
}, []);

persist 사용 (자동)

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";

const useAuthStore = create(
    persist(
        (set) => ({
            accessToken: null,
            setToken: (token) => set({ accessToken: token }),
            // AsyncStorage 저장/복원은 persist가 자동 처리
        }),
        {
            name: "auth-storage",  // AsyncStorage 키
            storage: createJSONStorage(() => AsyncStorage),
        }
    )
);

4. 실제 예시 (authStore)

// src/stores/authStore.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { UserInfo } from "../types/api.types";

interface AuthState {
    // 상태
    accessToken: string | null;
    userInfo: UserInfo | null;
    isLoggedIn: boolean;
    isHydrated: boolean;

    // 액션
    setAccessToken: (token: string) => void;
    setUserInfo: (info: UserInfo) => void;
    login: (token: string, userInfo: UserInfo) => void;
    logout: () => void;
    setHydrated: (hydrated: boolean) => void;
}

export const useAuthStore = create<AuthState>()(
    persist(
        (set) => ({
            // 초기 상태
            accessToken: null,
            userInfo: null,
            isLoggedIn: false,
            isHydrated: false,

            // 액션
            setAccessToken: (token) =>
                set({ accessToken: token, isLoggedIn: true }),

            setUserInfo: (info) => set({ userInfo: info }),

            login: (token, userInfo) =>
                set({
                    accessToken: token,
                    userInfo,
                    isLoggedIn: true,
                }),

            logout: () =>
                set({
                    accessToken: null,
                    userInfo: null,
                    isLoggedIn: false,
                }),

            setHydrated: (hydrated) => set({ isHydrated: hydrated }),
        }),
        {
            name: "auth-storage",
            storage: createJSONStorage(() => AsyncStorage),
            // isHydrated는 persist하지 않음
            partialize: (state) => ({
                accessToken: state.accessToken,
                userInfo: state.userInfo,
                isLoggedIn: state.isLoggedIn,
            }),
            onRehydrateStorage: () => (state) => {
                state?.setHydrated(true);
            },
        }
    )
);

// 스토어 외부에서 상태 접근용 (axios 인터셉터 등)
export const getAuthState = () => useAuthStore.getState();

5. persist 옵션

옵션 설명
name AsyncStorage에 저장될 키 이름
storage 저장소 (AsyncStorage, localStorage 등)
partialize 저장할 상태만 선택 (일부 제외 가능)
onRehydrateStorage 복원 완료 시 호출되는 콜백

6. Hydration (복원) 처리

앱 시작 시 AsyncStorage에서 데이터를 복원하는 데 시간이 걸립니다. 이를 Hydration이라고 합니다.

// 컴포넌트에서 hydration 완료 대기
const isHydrated = useAuthStore((state) => state.isHydrated);

useEffect(() => {
    if (!isHydrated) {
        return;  // 아직 복원 중
    }
    // hydration 완료 후 로직 실행
}, [isHydrated]);

7. 컴포넌트에서 사용

import { useAuthStore } from "../stores/authStore";

const MyComponent = () => {
    // 상태 읽기 (동기적)
    const { isLoggedIn, userInfo, accessToken } = useAuthStore();

    // 액션 호출 (자동으로 AsyncStorage에도 저장됨)
    const { login, logout } = useAuthStore();

    const handleLogin = () => {
        login("token123", { userId: 1, userName: "홍길동", userType: "WORKER" });
    };

    return (
        <View>
            {isLoggedIn ? (
                <Text>환영합니다, {userInfo?.userName}!</Text>
            ) : (
                <Button onPress={handleLogin} title="로그인" />
            )}
        </View>
    );
};

8. 컴포넌트 외부에서 사용 (axios 인터셉터)

// src/api/axios.ts
import { getAuthState } from "../stores/authStore";

// 요청 인터셉터: 토큰 자동 첨부 (동기적)
api.interceptors.request.use((config) => {
    const { accessToken } = getAuthState();  // 동기 접근!
    if (accessToken && config.headers) {
        config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
});

핵심: getAuthState()로 동기적으로 상태 접근 가능 (AsyncStorage는 비동기)


9. 기존 코드 vs Zustand + persist

항목 기존 (AsyncStorage 직접) Zustand + persist
읽기 await AsyncStorage.getItem() useAuthStore().accessToken
쓰기 await AsyncStorage.setItem() setAccessToken(token)
동기/비동기 비동기 동기 (persist는 백그라운드)
리렌더링 수동 처리 필요 자동
코드 간결성 길고 복잡 간결

핵심 포인트

  • persist = Zustand 상태를 스토리지에 자동 저장/복원하는 미들웨어
  • Hydration = 앱 시작 시 AsyncStorage에서 복원하는 과정, 완료 대기 필요
  • getAuthState() = 컴포넌트 외부에서 동기적으로 상태 접근
  • partialize = 저장할 상태만 선택 (isHydrated 같은 런타임 상태 제외)