리액트 공부하기 - 전역 상태 관리 라이브러리 Zustand
context API는 익혔으니
뭔가 실무적으로 사용할 만 한 전역 상태관리를 공부 해보려고 하는데
리덕스.. 는 쳐다만 봐도 동기를 잃어서,,, 조교님이 추천해주시기도 하고, 요즘 핫하다는 zustand 공부하기
진짜 공부해보니까 정~말 간단하고 좋으네욥.
우선 공식 문서 읽어보기 !
GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React
🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.
github.com
똑똑한 곰돌이.
Zustand는 정말 배우기 쉬운 상태 관리 라이브러리
React에서 useState나 useContext 써본 경험이 있다면 아주 금방 익힐 수 있다.
useState와 useContext가 믹스된 느낌이랄까..
1단계: 기본 개념 이해하기
✔️ Zustand란?
- 독일어로 "상태"라는 뜻
- 가볍고 빠른 React 상태 관리 라이브러리
- Redux처럼 전역 상태를 다룰 수 있지만 코드가 훨씬 짧고 직관적
2단계: 기본 사용법 익히기
1. 라이브러리
npm install zustand
2. store 만들기
// store.js
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}))
export default useStore
zustand의 store는 거의 context API의 create이랑 provider가 합쳐진 느낌이네..
아래는 위 store에 대한 코드 설명이다. ↓
// store.js
이건 파일 이름이야. zustand로 만든 전역 상태(store)를 여기에 정의하는 거야.
다른 컴포넌트에서 불러와서 사용할 수 있게 만들어.
import { create } from 'zustand'
zustand에서 가장 중요한 함수인 create()를 불러오는 코드야.
이 함수로 상태 저장소(store)를 만들 수 있어.
const useStore = create((set) => ({
- create() 함수를 호출하면서 안에 상태 객체를 정의하는 함수를 넣고 있어.
- 이 함수는 set이라는 함수를 받아.
👉 이 set은 상태를 업데이트할 때 사용하는 함수야.- 결과적으로 useStore는 우리가 만든 커스텀 훅이 되고,
컴포넌트에서 이걸 쓰면 상태와 상태를 바꾸는 함수들을 쓸 수 있게 돼.
count: 0,
상태의 기본값이야.
count라는 이름의 숫자 상태를 만들고, 초기값은 0으로 설정한 거야.
increase: () => set((state) => ({ count: state.count + 1 })),
increase는 함수야.
이걸 실행하면 set()을 이용해서 현재 상태의 count 값을 +1 해줘.
- (state) => ({ count: state.count + 1 }) 이 부분은 현재 상태를 받아서 count를 증가시킨 새 객체를 반환하는 거야.
- 즉, count 값을 1 증가시키는 함수!
reset: () => set({ count: 0 }),
reset도 함수야.
실행하면 set()을 이용해서 count 값을 0으로 리셋해.
상태 객체 전체를 { count: 0 }로 바꾸는 거야.
}))
create 함수 안에 있는 객체 정의가 끝났다는 뜻이야.
이제 이 상태와 상태 변경 함수들이 저장소에 잘 등록됐어.
export default useStore
useStore 훅을 밖에서 쓸 수 있도록 내보내는 코드야.
다른 컴포넌트에서 이걸 import useStore from './store' 이렇게 불러와서 사용할 수 있어.
📦 이 코드를 한 줄로 요약하면?
count 값을 전역에서 관리하면서 증가시키거나 초기화할 수 있는 상태 저장소(store)를 만든 것
3. 컴포넌트에서 사용하기
import useStore from './store'
function Counter() {
const { count, increase, reset } = useStore()
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>+1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
3단계: 파악해야 할 주요 개념
create() | store를 생성하는 함수 |
set() | 상태를 변경할 때 사용 |
get() | 현재 상태 가져올 때 사용 |
subscribe() | 상태 변화 감지 |
persist 미들웨어 | 상태를 localStorage에 저장 가능 |
devtools 미들웨어 | Redux DevTools 사용 가능 |
4단계: 미들웨어 적용하기 (옵션)
localStorage 저장 (persist)
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(persist(
(set) => ({
darkMode: false,
toggle: () => set((state) => ({ darkMode: !state.darkMode }))
}),
{
name: 'settings', // localStorage key 이름
}
))
상태 변경 함수나, 상태가 여러개 있다면?
여러 상태 사용하는 예시
// src/stores/useAppStore.js
import { create } from 'zustand';
const useAppStore = create((set) => ({
// 상태들
count: 0,
isLoggedIn: false,
username: '',
// 상태 바꾸는 함수들
increase: () => set((state) => ({ count: state.count + 1 })),
login: (username) => set(() => ({ isLoggedIn: true, username })),
logout: () => set(() => ({ isLoggedIn: false, username: '' })),
}));
export default useAppStore;
사용 방법 (리액트 컴포넌트에서)
import useAppStore from './stores/useAppStore';
function MyComponent() {
const { count, increase, isLoggedIn, login, logout } = useAppStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increase}>+1</button>
{!isLoggedIn ? (
<button onClick={() => login('Jane')}>Login</button>
) : (
<div>
<p>Welcome, {useAppStore.getState().username}!</p>
<button onClick={logout}>Logout</button>
</div>
)}
</div>
);
}
store 코드 설명
구조 먼저 보기
increase: () => set((state) => ({ count: state.count + 1 })),
login: (username) => set(() => ({ isLoggedIn: true, username })),
logout: () => set(() => ({ isLoggedIn: false, username: '' })),
위에 있는 각각은 함수예요.
괄호 () 안에는 그 함수에 전달할 매개변수(파라미터) 를 적어요.
✔️ 하나씩 해석해볼게요:
1. increase: () => set((state) => ({ count: state.count + 1 }))
- increase는 매개변수가 필요 없어요.
- 그냥 현재 state.count 값을 1 올리기만 하면 되니까 괄호 안이 비어 있어요.
→ 그래서 ()는 아무것도 받지 않는 함수를 뜻해요.
2. login: (username) => set(() => ({ isLoggedIn: true, username }))
- login은 사용자의 이름(또는 ID) 같은 정보를 받아서 로그인 상태를 저장해야 해요.
- 그래서 (username) 이라는 매개변수(parameter) 를 받는 거예요.
→ 즉, 이 함수는 호출할 때 이렇게 써요: login('Jane')
3. logout: () => set(() => ({ isLoggedIn: false, username: '' }))
- logout은 아무 정보도 필요 없이 로그아웃만 시키면 되니까 매개변수가 없어요.
정리하자면:
- () 안에는 그 함수가 필요로 하는 값(매개변수) 을 넣어요.
- 필요하면 (username)처럼 이름을 정해서 받고,
- 필요 없으면 그냥 ()만 써요.
갑자기 매개 변수 개념도 헷갈리기 시작..
💡 매개변수란?
함수가 사용할 수 있도록 '외부에서 주는 값'이야.
함수를 만들 때 괄호 ( ) 안에 쓰는 이름이 매개변수(parameter) 예요.
예시:
function sayHello(name) {
console.log('Hello, ' + name);
}
- 여기서 name이 매개변수(parameter) 예요.
- 이 함수는 name이라는 이름으로 어떤 값을 받을 준비를 하고 있는 거예요.
💡 함수 호출할 땐?
sayHello('Jane');
- 이때 'Jane'은 인자(argument) 라고 해요.
- name이라는 매개변수 자리에 들어갈 실제 값이죠!
🎨 그림처럼 이해하기
function greet(who) {
console.log("Hi " + who);
}
greet("Jane");
- who → 매개변수(parameter) : 함수를 만들 때 정의한 이름
- "Jane" → 인자(argument) : 함수를 부를 때 전달하는 실제 값
✔️ Zustand 예제로 다시 연결하면:
login: (username) => set(() => ({ isLoggedIn: true, username })),
- 여기서 username은 매개변수(parameter) 에요.
- 함수 실행할 때 login("Jane") 이런 식으로 값을 넘기면
- username 자리에 "Jane"이 들어가요.
set((state) => ({ count: state.count + 1 })) // 함수형 업데이트
vs
set({ loading: true }) // 직접 값 전달
이 두 코드의 차이가 뭐지? 왜 set 함수 쓰는 법이 다르지?
결론부터 말하면:
set 함수는 두 가지 방식으로 사용할 수 있기 때문이야!
🔍 두 가지 방식 설명
1. 객체 직접 전달
set({ loading: true });
- 그냥 새로운 상태를 통째로 넘기는 방식이야.
- 이 경우에는 이전 상태는 신경 안 써도 될 때 사용해.
- 상태를 "단순히 바꿔치기" 할 때 좋아!
2. 함수형 업데이트
set((state) => ({ count: state.count + 1 }));
- 현재 상태 state를 기반으로 새로운 상태를 계산할 때 사용해.
- 상태를 누적하거나 이전 값을 참고할 때 꼭 필요해.
- 예: count += 1, list.push(...)처럼 이전 상태가 필요할 때!
✨ 예시
// 버튼 클릭 시 로딩 상태 설정
startLoading: () => set({ loading: true })
// 버튼 클릭 시 카운트 증가
increase: () => set((state) => ({ count: state.count + 1 }))
zustand는 provider로 감쌀 필요 없어?
Zustand는 Provider로 감쌀 필요 없어?
✔️ 네! Zustand는 Provider가 필요 없습니다.
이게 zustand의 가장 큰 장점 중 하나예요.
- React 트리 어딘가에 따로 <Provider>로 감싸는 구조 필요 없음
- 어떤 컴포넌트든 그냥 useStore() 훅만 쓰면 전역 상태 접근 가능
예:
// 그냥 아무 곳에서나
const count = useStore((state) => state.count)
보너스: 왜 이게 좋은가요?
- 렌더링 최적화: Context는 전역 상태 하나 바뀌면 모든 구독 컴포넌트가 다시 렌더링됨 😵
→ Zustand는 구독 단위로만 리렌더링되기 때문에 성능이 훨씬 좋음 - 간결한 코드: boilerplate 없이 바로 store 생성 + 사용
주의! zustand 상태 안에.. useEffect를 사용 하면 안됌
useEffect는 zustand 스토어 안에서는 쓰면 안 돼!
useEffect는 React 컴포넌트 안에서만 작동하는 훅이야.
근데 zustand store는 React 컴포넌트가 아니야, 그냥 JavaScript 함수야.
그래서 아래처럼 하면 에러 나:
// ❌ 이렇게 하면 안 돼
const useUserStore = create((set) => {
useEffect(() => {
// 뭔가 하려고 해도 작동 안 함
}, []);
return {
user: null,
...
};
});
올바른 패턴: useEffect는 컴포넌트에서 호출하고, zustand의 메서드만 불러!
// useUserStore.js
const useUserStore = create((set) => ({
user: null,
fetchUser: async () => {
const result = await getUserInfo();
set({ user: result?.user || null });
},
}));
// SomeComponent.jsx
import { useEffect } from "react";
import useUserStore from "@/stores/useUserStore";
export default function SomeComponent() {
const fetchUser = useUserStore((state) => state.fetchUser);
useEffect(() => {
fetchUser(); // ✅ 여기서 호출!
}, []);
return <div>...</div>;
}
출처 : zustand 공식 문서, 챗지피티