Zustand는 보일러플레이트 없이 전역 상태를 관리할 수 있는 경량 라이브러리입니다.

Redux의 복잡함, Context API의 성능 한계가 고민이라면 Zustand가 좋은 대안입니다. 설정이 거의 없고, 코드량도 적으며, 자동으로 리렌더링을 최적화합니다.


설치와 기본 사용

BASH
npm install zustand
TSX
import { create } from 'zustand';

// 스토어 정의 — 상태와 액션을 한 곳에
interface CounterStore {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

// 사용 — Hook처럼 사용
function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);

  return (
    <View>
      <Text>{count}</Text>
      <Pressable onPress={increment}><Text>+1</Text></Pressable>
    </View>
  );
}

Redux와 비교하면 action type, reducer, dispatch가 전부 필요 없습니다. set 함수 하나로 상태 업데이트가 끝납니다.


실전: 인증 스토어

TSX
import { create } from 'zustand';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthStore {
  user: User | null;
  token: string | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  setUser: (user: User) => void;
}

const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  token: null,
  isLoading: false,

  login: async (email, password) => {
    set({ isLoading: true });
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      });
      const { user, token } = await response.json();
      set({ user, token, isLoading: false });
    } catch (error) {
      set({ isLoading: false });
      throw error;
    }
  },

  logout: () => set({ user: null, token: null }),
  setUser: (user) => set({ user }),
}));

컴포넌트에서 사용

TSX
function LoginScreen() {
  const { login, isLoading } = useAuthStore();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    try {
      await login(email, password);
      // 로그인 성공 — 네비게이션은 자동 전환
    } catch {
      Alert.alert('오류', '로그인에 실패했습니다');
    }
  };

  return (
    <View>
      <TextInput value={email} onChangeText={setEmail} />
      <TextInput value={password} onChangeText={setPassword} secureTextEntry />
      <Pressable onPress={handleLogin} disabled={isLoading}>
        <Text>{isLoading ? '로그인 중...' : '로그인'}</Text>
      </Pressable>
    </View>
  );
}

function Header() {
  // 필요한 상태만 구독 — user만 변경될 때 리렌더링
  const userName = useAuthStore((state) => state.user?.name);
  return <Text>{userName ?? '로그인하세요'}</Text>;
}

Selector로 리렌더링 최적화

TSX
// 나쁜 예: 전체 스토어 구독 → 어떤 상태든 변하면 리렌더링
function Bad() {
  const store = useAuthStore(); // 전체 구독
  return <Text>{store.user?.name}</Text>;
}

// 좋은 예: 필요한 것만 구독
function Good() {
  const name = useAuthStore((state) => state.user?.name);
  return <Text>{name}</Text>;
}

// 여러 값을 한번에 선택 (shallow 비교 필요)
import { useShallow } from 'zustand/react/shallow';

function MultiSelect() {
  const { user, isLoading } = useAuthStore(
    useShallow((state) => ({
      user: state.user,
      isLoading: state.isLoading,
    }))
  );
  return <Text>{isLoading ? '로딩중' : user?.name}</Text>;
}

Persist 미들웨어 — 상태 영속화

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

interface SettingsStore {
  isDarkMode: boolean;
  language: string;
  toggleDarkMode: () => void;
  setLanguage: (lang: string) => void;
}

const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({
      isDarkMode: false,
      language: 'ko',
      toggleDarkMode: () => set((s) => ({ isDarkMode: !s.isDarkMode })),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: 'settings-storage',
      // React Native에서는 AsyncStorage 사용
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

스토어 분리 패턴

TSX
// stores/useAuthStore.ts — 인증
const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  token: null,
  login: async () => { /* ... */ },
  logout: () => set({ user: null, token: null }),
}));

// stores/useCartStore.ts — 장바구니
const useCartStore = create<CartStore>((set, get) => ({
  items: [],
  addItem: (item) => set((s) => ({
    items: [...s.items, item],
  })),
  removeItem: (id) => set((s) => ({
    items: s.items.filter((i) => i.id !== id),
  })),
  // get()으로 현재 상태 읽기
  total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));

// stores/useNotificationStore.ts — 알림
const useNotificationStore = create<NotificationStore>((set) => ({
  unreadCount: 0,
  incrementUnread: () => set((s) => ({ unreadCount: s.unreadCount + 1 })),
  clearUnread: () => set({ unreadCount: 0 }),
}));

스토어 외부에서 상태 접근

TSX
// 컴포넌트 밖에서도 상태를 읽거나 변경 가능
// API 인터셉터, 네비게이션 가드 등에 유용

// 상태 읽기
const token = useAuthStore.getState().token;

// 상태 변경
useAuthStore.setState({ user: null });

// 구독
const unsubscribe = useAuthStore.subscribe((state) => {
  console.log('상태 변경:', state);
});

Zustand vs Redux vs Context 비교

기준ZustandRedux ToolkitContext API
보일러플레이트최소중간낮음
번들 크기~1KB~11KB0 (내장)
DevTools지원지원제한적
미들웨어persist, devtools 등풍부없음
리렌더링 최적화자동 (selector)자동수동
비동기 처리직접 async/awaitcreateAsyncThunk직접

정리

  • Zustand는 최소한의 코드로 전역 상태를 관리 할 수 있습니다
  • Selector 를 사용해 필요한 상태만 구독하면 불필요한 리렌더링을 방지합니다
  • persist 미들웨어 + AsyncStorage로 앱 재시작 후에도 상태를 유지할 수 있습니다
  • 스토어를 ** 도메인별로 분리 **하면 관리가 편해집니다
  • 컴포넌트 밖에서도 getState(), setState()로 상태에 접근할 수 있습니다
댓글 로딩 중...