Zustand 완벽 가이드 | 간단한 상태 관리·React·TypeScript·Middleware·실전 활용

Zustand 완벽 가이드 | 간단한 상태 관리·React·TypeScript·Middleware·실전 활용

이 글의 핵심

Zustand로 간단한 상태 관리를 구현하는 완벽 가이드입니다. Redux 없이 가볍게, TypeScript 지원, Middleware, Persist까지 실전 예제로 정리했습니다.

실무 경험 공유: Redux에서 Zustand로 전환하면서, 보일러플레이트가 90% 감소하고 번들 크기가 50% 줄어든 경험을 공유합니다.

들어가며: “Redux가 복잡해요”

실무 문제 시나리오

시나리오 1: 보일러플레이트가 너무 많아요
Redux는 복잡합니다. Zustand는 간단합니다.

시나리오 2: 번들 크기가 커요
Redux는 무겁습니다. Zustand는 1KB입니다.

시나리오 3: TypeScript 설정이 어려워요
Redux는 타입 설정이 복잡합니다. Zustand는 자동 추론됩니다.


1. Zustand란?

핵심 특징

Zustand는 간단한 React 상태 관리 라이브러리입니다.

주요 장점:

  • 간단한 API: 보일러플레이트 없음
  • 작은 크기: 1KB
  • TypeScript: 완벽한 지원
  • Middleware: Persist, Devtools
  • React 외부: Vanilla JS 사용 가능

2. 기본 사용

설치

npm install zustand

Store 생성

// store/useStore.ts
import { create } from 'zustand';

interface Store {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

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

컴포넌트에서 사용

// components/Counter.tsx
import { useStore } from '../store/useStore';

export default function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  const decrement = useStore((state) => state.decrement);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

3. 고급 패턴

Async Actions

interface UserStore {
  users: User[];
  loading: boolean;
  error: string | null;
  fetchUsers: () => Promise<void>;
}

export const useUserStore = create<UserStore>((set) => ({
  users: [],
  loading: false,
  error: null,

  fetchUsers: async () => {
    set({ loading: true, error: null });

    try {
      const response = await fetch('/api/users');
      const users = await response.json();
      set({ users, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
}));

Computed Values

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  total: () => number;
}

export const useCartStore = create<CartStore>((set, get) => ({
  items: [],

  addItem: (item) =>
    set((state) => ({ items: [...state.items, item] })),

  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter((item) => item.id !== id),
    })),

  total: () => {
    const { items } = get();
    return items.reduce((sum, item) => sum + item.price, 0);
  },
}));

4. Middleware

Persist

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface AuthStore {
  user: User | null;
  token: string | null;
  login: (user: User, token: string) => void;
  logout: () => void;
}

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

      login: (user, token) => set({ user, token }),
      logout: () => set({ user: null, token: null }),
    }),
    {
      name: 'auth-storage',
    }
  )
);

Devtools

import { devtools } from 'zustand/middleware';

export const useStore = create<Store>()(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    { name: 'CounterStore' }
  )
);

Immer

import { immer } from 'zustand/middleware/immer';

export const useStore = create<Store>()(
  immer((set) => ({
    nested: { deep: { value: 0 } },
    updateDeep: (value: number) =>
      set((state) => {
        state.nested.deep.value = value;
      }),
  }))
);

5. Slice Pattern

// store/slices/userSlice.ts
export const createUserSlice = (set, get) => ({
  users: [],
  fetchUsers: async () => {
    const users = await api.getUsers();
    set({ users });
  },
});

// store/slices/cartSlice.ts
export const createCartSlice = (set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
});

// store/index.ts
import { create } from 'zustand';
import { createUserSlice } from './slices/userSlice';
import { createCartSlice } from './slices/cartSlice';

export const useStore = create((set, get) => ({
  ...createUserSlice(set, get),
  ...createCartSlice(set, get),
}));

6. Selector 최적화

잘못된 예

// 전체 store를 구독 (불필요한 리렌더링)
const store = useStore();

올바른 예

// 필요한 값만 구독
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);

Shallow 비교

import { shallow } from 'zustand/shallow';

const { count, increment } = useStore(
  (state) => ({ count: state.count, increment: state.increment }),
  shallow
);

7. Vanilla JS

import { createStore } from 'zustand/vanilla';

const store = createStore<Store>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// 구독
const unsubscribe = store.subscribe((state) => {
  console.log('Count:', state.count);
});

// 사용
store.getState().increment();
console.log(store.getState().count); // 1

// 구독 해제
unsubscribe();

정리 및 체크리스트

핵심 요약

  • Zustand: 간단한 상태 관리
  • 작은 크기: 1KB
  • TypeScript: 완벽한 지원
  • Middleware: Persist, Devtools, Immer
  • Selector: 최적화 가능
  • Vanilla JS: React 외부 사용

구현 체크리스트

  • Zustand 설치
  • Store 생성
  • 컴포넌트 연결
  • Async Actions 구현
  • Middleware 추가
  • Selector 최적화
  • Slice Pattern 적용

같이 보면 좋은 글

  • React Native 완벽 가이드
  • Next.js App Router 가이드
  • TypeScript 완벽 가이드

이 글에서 다루는 키워드

Zustand, State Management, React, TypeScript, Redux, Frontend, Performance

자주 묻는 질문 (FAQ)

Q. Redux와 비교하면 어떤가요?

A. Zustand가 훨씬 간단하고 가볍습니다. Redux는 더 많은 기능을 제공하지만 복잡합니다.

Q. Context API와 비교하면 어떤가요?

A. Zustand가 성능이 더 좋고 사용이 간편합니다. Context API는 prop drilling을 피하는 용도로 적합합니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 많은 기업에서 안정적으로 사용하고 있습니다.

Q. SSR을 지원하나요?

A. 네, Next.js와 함께 사용할 수 있습니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3