| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- CSS
- javascript
- vscode
- html
- Eclipse
- 정규식
- string
- 자바문법
- 스프링부트
- junit5
- Java
- input
- 테스트자동화
- 문자열
- Array
- list
- 자바
- IntelliJ
- 자바스크립트
- 인텔리제이
- HashMap
- 단위테스트
- js
- junit
- 배열
- SpringBoot
- Visual Studio Code
- java테스트
- ArrayList
- math
- Today
- Total
어제 오늘 내일
[ React ] Zustand 사용법 : React 상태 관리 가볍게 시작하기 본문
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 zustandProvider로 앱을 감쌀 필요도 없고, 별도의 설정 파일도 필요 없습니다.
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와 비교
| 항목 | Zustand | Redux (Toolkit) | Context API |
| 보일러플레이트 | 거의 없음 | action/slice 정의 필요 | Provider 구성 필요 |
| Provider 필요 여부 | 불필요 | 필요 | 필요 |
| 선택적 리렌더링 | 기본 지원 | 기본 지원 | 직접 최적화 필요 |
| 러닝 커브 | 낮음 | 중간~높음 | 낮음 |
| 미들웨어 생태계 | 가볍고 필요한 것만 | 풍부하고 성숙함 | 없음 |
작은~중간 규모 프로젝트나, Redux의 구조가 과하게 느껴지는 상황이라면 Zustand가 좋은 대안이 됩니다. 반면 대규모 팀에서 엄격한 상태 변경 규칙과 미들웨어 생태계가 필요하다면 Redux Toolkit이 여전히 강력한 선택지입니다.
8. 마무리
Zustand는 "전역 상태 관리가 꼭 이렇게 복잡해야 할까?"라는 질문에 대한 실용적인 답에 가깝습니다. Provider 없이 훅 하나로 스토어를 만들고, selector로 필요한 값만 구독해서 성능까지 챙길 수 있다는 점이 가장 큰 매력입니다. Context API로 props drilling만 겨우 해결하고 있었다면, 혹은 Redux가 프로젝트 규모에 비해 과하다고 느꼈다면 Zustand 도입을 검토해볼 만합니다.
'IT > React' 카테고리의 다른 글
| [React] TanStack Query 사용법: 서버 상태 관리 제대로 하기 (0) | 2026.08.08 |
|---|---|
| clsx로 React 조건부 클래스 깔끔하게 관리하기 (0) | 2026.08.07 |
| tailwind-merge로 Tailwind 클래스 충돌 깔끔하게 해결하기 (0) | 2026.08.07 |
| React에서 Tailwind CSS 제대로 활용하기 (설치, 활용) (0) | 2026.08.07 |
| [ React ] useReducer 사용법: 복잡한 상태 로직을 체계적으로 관리하기 (0) | 2026.08.07 |