
이 글에서는 Zustand라는 상태 관리 라이브러리를 처음부터 차근차근 보기만 해도 이해할 수 있도록 정리해봤습니다.
상태는 애플리케이션의 현재 상황을 나타내는 데이터입니다.
예를 들어보겠습니다:
해결책 💡
장바구니 상태를 상품 상세 페이지에서 관리한 뒤, 이를 헤더까지 전달하기 위해 React의 props를 사용할 수 있습니다.
props 전달 📦
상품 상세 페이지 → 상품 리스트 페이지 → 메인 페이지 → 헤더와 같은 순서로 props를 통해 상태를 전달할 수 있습니다.
문제점 🚧
그러나 앱의 규모가 커지고 컴포넌트가 깊어질수록 props를 단계별로 계속 전달해야 하는 문제가 발생합니다. 이 현상을 props drilling이라고 합니다.
이런 props drilling 문제를 깔끔하고 효율적으로 해결하기 위해 등장한 것이 바로 전역 상태 관리입니다. 전역 상태 관리를 사용하면 필요한 상태를 컴포넌트 어디에서나 손쉽게 접근하고 관리할 수 있습니다.
이제, 이런 전역 상태 관리 방식을 간단하고 효과적으로 구현할 수 있는 라이브러리 중 하나인 Zustand를 살펴보겠습니다.
스토어는 모든 상태와 상태를 변경하는 함수들을 담는 곳입니다.
import { create } from 'zustand'
// 스토어 생성
const useCounterStore = create((set, get) => ({
// 상태 (데이터)
count: 0,
// 액션 (상태를 변경하는 함수들)
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
set 함수: 현재 상태를 업데이트하는 함수입니다.
// 방법 1: 직접 값 설정
set({ count: 0 }) // count를 0으로 설정
// 방법 2: 이전 상태를 기반으로 업데이트
set((state) => ({ count: state.count + 1 })) // 현재 count에 1을 더함
왜
(state) => ({ count: state.count + 1 })인가요?
이것은 함수형 업데이트입니다. 현재 상태를 받아서 새로운 상태를 반환하는 방식입니다.
// ❌ 잘못된 방법 (이전 상태를 모름)
set({ count: count + 1 }) // count가 정의되지 않음!
// ✅ 올바른 방법 (이전 상태를 기반으로 업데이트)
set((state) => ({ count: state.count + 1 })) // 현재 state.count에 1을 더함
get 함수: 현재 상태를 가져오는 함수입니다.
const useCounterStore = create((set, get) => ({
count: 0,
// 현재 상태를 확인하고 조건부로 업데이트
incrementIfLessThanTen: () => {
const currentCount = get().count // 현재 count 값 가져오기
if (currentCount < 10) {
set({ count: currentCount + 1 })
}
},
// 현재 상태를 로그로 출력
logCurrentState: () => {
console.log('현재 상태:', get()) // 전체 상태 출력
}
}))
import { create } from 'zustand'
// 1. 스토어 생성
const useCounterStore = create((set, get) => ({
// 상태
count: 0,
// 액션들
increment: () => {
console.log('증가 버튼 클릭됨!')
set((state) => ({
count: state.count + 1
}))
},
decrement: () => {
console.log('감소 버튼 클릭됨!')
set((state) => ({
count: state.count - 1
}))
},
reset: () => {
console.log('리셋 버튼 클릭됨!')
set({ count: 0 })
},
// 현재 상태 확인
getCurrentCount: () => {
const current = get().count
console.log('현재 카운트:', current)
return current
}
}))
// 2. 컴포넌트에서 사용
function Counter() {
// 스토어에서 필요한 것들 가져오기
const { count, increment, decrement, reset } = useCounterStore()
return (
<div>
<h2>카운터: {count}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>리셋</button>
</div>
)
}
create 함수로 스토어를 만듭니다count: 0으로 초기 상태를 설정합니다increment, decrement, reset 함수를 만듭니다useCounterStore()로 스토어를 가져옵니다set 함수로 상태가 변경됩니다성능 최적화를 위해서입니다. 컴포넌트가 필요한 상태만 구독하도록 하면, 다른 상태가 변경되어도 불필요한 리렌더링을 방지할 수 있습니다.
// ❌ 전체 스토어 구독 (비효율적)
function UserProfile() {
const { user, cart, settings, notifications } = useCounterStore()
// user만 사용하는데 cart, settings, notifications도 구독함
// 다른 상태가 변경되면 이 컴포넌트도 리렌더링됨
return <div>안녕하세요, {user.name}님!</div>
}
// ✅ 선택적 구독 (효율적)
function UserProfile() {
const user = useCounterStore((state) => state.user)
// user만 구독하므로 다른 상태가 변경되어도 리렌더링되지 않음
return <div>안녕하세요, {user.name}님!</div>
}
const useUserStore = create((set, get) => ({
user: null,
loading: false,
error: null,
// 비동기 로그인 액션
loginAsync: async (email, password) => {
set({ loading: true, error: null }) // 로딩 시작
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (!response.ok) {
throw new Error('로그인 실패')
}
const userData = await response.json()
// 성공 시 상태 업데이트
set({
user: userData,
loading: false,
error: null
})
} catch (error) {
// 실패 시 에러 상태 업데이트
set({
loading: false,
error: error.message
})
}
},
// 사용자 정보 가져오기
fetchUser: async (userId) => {
set({ loading: true })
try {
const response = await fetch(`/api/users/${userId}`)
const user = await response.json()
set({ user, loading: false })
} catch (error) {
set({ loading: false, error: error.message })
}
},
}))
function LoginForm() {
const { loginAsync, loading, error } = useUserStore()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
await loginAsync(email, password)
}
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="이메일"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="비밀번호"
/>
<button type="submit" disabled={loading}>
{loading ? '로그인 중...' : '로그인'}
</button>
</form>
)
}
Zustand는 정말 간단하면서도 강력한 상태 관리 라이브러리입니다.
create: 스토어를 만드는 함수set: 상태를 업데이트하는 함수get: 현재 상태를 가져오는 함수