어제 오늘 내일

[ React ] Zustand 사용법 : React 상태 관리 가볍게 시작하기 본문

IT/React

[ React ] Zustand 사용법 : React 상태 관리 가볍게 시작하기

hi.anna 2026. 8. 8. 22:13

1. 들어가며: 어떤 문제를 해결하는가

React에서 여러 컴포넌트가 같은 상태를 공유해야 할 때, 가장 먼저 마주치는 선택지는 useState를 최상위 컴포넌트로 끌어올리고 props로 내려주는 방식입니다. 하지만 컴포넌트 트리가 깊어지면 중간 컴포넌트들이 사용하지도 않는 props를 계속 전달만 하는 props drilling 문제가 생깁니다.

Context API로 이 문제를 해결할 수도 있지만, Context는 값이 하나라도 바뀌면 그 Context를 구독하는 모든 컴포넌트가 리렌더링된다는 한계가 있습니다. Redux는 이런 문제를 정교하게 해결하지만, 그 대가로 action, reducer, dispatch 같은 보일러플레이트 코드가 많이 필요합니다.
Zustand는 이 사이 어딘가에 있는 선택지입니다. Redux만큼 정교한 렌더링 최적화를 제공하면서도, 보일러플레이트는 거의 없이 몇 줄만으로 전역 상태를 만들 수 있습니다.

import { create } from 'zustand'

const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

이 짧은 코드 하나로 어떤 컴포넌트에서든 접근 가능한 전역 카운터 스토어가 완성됩니다.

2. 설치하기

npm install zustand

Provider로 앱을 감쌀 필요도 없고, 별도의 설정 파일도 필요 없습니다.

3. 기본 사용법: 스토어 만들고 사용하기

스토어 생성

create 함수에 상태와 상태를 변경하는 함수(action)를 함께 정의합니다.

// store/counterStore.js
import { create } from 'zustand'

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

set은 상태를 갱신하는 함수입니다. 콜백 형태로 넘기면 이전 상태(state)를 받아 다음 상태를 계산할 수 있고, 객체를 바로 넘기면 해당 값으로 덮어씁니다.

컴포넌트에서 사용

만들어둔 훅을 그냥 호출하기만 하면 됩니다.

function Counter() {
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  const decrement = useCounterStore((state) => state.decrement)

  return (
    <div className="flex items-center gap-4">
      <button onClick={decrement} className="px-3 py-1 bg-gray-200 rounded">-</button>
      <span className="text-xl font-bold">{count}</span>
      <button onClick={increment} className="px-3 py-1 bg-gray-200 rounded">+</button>
    </div>
  )
}

Context Provider로 감쌀 필요 없이, 어느 컴포넌트에서든 useCounterStore를 불러오기만 하면 같은 상태에 접근할 수 있습니다.

4. 선택적 구독으로 불필요한 리렌더링 막기

Zustand의 핵심 장점은 selector를 통해 스토어의 일부 값만 구독할 수 있다는 점입니다. 위 예제에서 useCounterStore((state) => state.count)처럼 함수를 넘기면, 해당 컴포넌트는 count 값이 바뀔 때만 리렌더링됩니다. 스토어에 다른 상태가 추가되고 그 값이 바뀌어도, count만 구독한 컴포넌트는 영향을 받지 않습니다.

const useAppStore = create((set) => ({
  count: 0,
  user: null,
  increment: () => set((state) => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
}))

// count가 바뀔 때만 리렌더링됨 (user가 바뀌어도 영향 없음)
function CountDisplay() {
  const count = useAppStore((state) => state.count)
  return <p>{count}</p>
}

여러 값을 한 번에 구독하고 싶다면 얕은 비교(shallow comparison)를 함께 사용하는 것이 안전합니다.

import { useShallow } from 'zustand/react/shallow'

function UserInfo() {
  const { name, email } = useAppStore(
    useShallow((state) => ({ name: state.user?.name, email: state.user?.email }))
  )
  return <p>{name} / {email}</p>
}

5. 비동기 액션 다루기

set을 호출하는 함수 안에서 자유롭게 async/await를 사용할 수 있습니다. 별도의 미들웨어나 thunk 개념이 필요 없습니다.

const useTodoStore = create((set) => ({
  todos: [],
  loading: false,
  fetchTodos: async () => {
    set({ loading: true })
    const res = await fetch('/api/todos')
    const data = await res.json()
    set({ todos: data, loading: false })
  },
}))
function TodoList() {
  const { todos, loading, fetchTodos } = useTodoStore()

  useEffect(() => {
    fetchTodos()
  }, [fetchTodos])

  if (loading) return <p>불러오는 중...</p>
  return (
    <ul>
      {todos.map((todo) => <li key={todo.id}>{todo.title}</li>)}
    </ul>
  )
}

6. 미들웨어 활용하기

persist: 로컬 스토리지에 상태 저장

새로고침해도 상태를 유지하고 싶다면 persist 미들웨어를 감싸주기만 하면 됩니다.

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

const useSettingsStore = create(
  persist(
    (set) => ({
      theme: 'light',
      toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
    }),
    { name: 'settings-storage' } // localStorage에 저장될 키 이름
  )
)

devtools: 개발자 도구 연동

Redux DevTools 확장 프로그램에서 상태 변화를 추적하고 싶다면 devtools 미들웨어를 추가합니다.

import { devtools } from 'zustand/middleware'

const useCounterStore = create(
  devtools((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
)

미들웨어는 여러 개를 겹쳐서 사용할 수도 있습니다.

create(devtools(persist((set) => ({ /* ... */ }), { name: 'store' })))

7. Redux, Context API와 비교

항목ZustandRedux (Toolkit)Context API
보일러플레이트거의 없음action/slice 정의 필요Provider 구성 필요
Provider 필요 여부불필요필요필요
선택적 리렌더링기본 지원기본 지원직접 최적화 필요
러닝 커브낮음중간~높음낮음
미들웨어 생태계가볍고 필요한 것만풍부하고 성숙함없음

작은~중간 규모 프로젝트나, Redux의 구조가 과하게 느껴지는 상황이라면 Zustand가 좋은 대안이 됩니다. 반면 대규모 팀에서 엄격한 상태 변경 규칙과 미들웨어 생태계가 필요하다면 Redux Toolkit이 여전히 강력한 선택지입니다.

8. 마무리

Zustand는 "전역 상태 관리가 꼭 이렇게 복잡해야 할까?"라는 질문에 대한 실용적인 답에 가깝습니다. Provider 없이 훅 하나로 스토어를 만들고, selector로 필요한 값만 구독해서 성능까지 챙길 수 있다는 점이 가장 큰 매력입니다. Context API로 props drilling만 겨우 해결하고 있었다면, 혹은 Redux가 프로젝트 규모에 비해 과하다고 느꼈다면 Zustand 도입을 검토해볼 만합니다.

반응형
Comments