Zustand: React 상태 관리

Yunsung·2025년 6월 26일
post-thumbnail

이 글에서는 Zustand라는 상태 관리 라이브러리를 처음부터 차근차근 보기만 해도 이해할 수 있도록 정리해봤습니다.

상태(State)란?

상태는 애플리케이션의 현재 상황을 나타내는 데이터입니다.

예를 들어보겠습니다:

  • 로그인 상태: 사용자가 로그인했는지, 안 했는지
  • 장바구니 상태: 어떤 상품들이 담겨있는지
  • 테마 상태: 다크모드인지, 라이트모드인지

왜 전역 상태 관리가 필요한가요?

쇼핑몰에서 사용자가 상품을 장바구니에 담으면, 장바구니 아이콘에 담긴 상품의 개수를 헤더에서 보여줘야 합니다. 하지만 장바구니에 상품을 추가하는 버튼은 상품 상세 페이지에 있을때

  • 해결책 💡
    장바구니 상태를 상품 상세 페이지에서 관리한 뒤, 이를 헤더까지 전달하기 위해 Reactprops를 사용할 수 있습니다.

  • props 전달 📦
    상품 상세 페이지 → 상품 리스트 페이지 → 메인 페이지 → 헤더와 같은 순서로 props를 통해 상태를 전달할 수 있습니다.

  • 문제점 🚧
    그러나 앱의 규모가 커지고 컴포넌트가 깊어질수록 props를 단계별로 계속 전달해야 하는 문제가 발생합니다. 이 현상을 props drilling이라고 합니다.

전역 상태 관리로 해결

이런 props drilling 문제를 깔끔하고 효율적으로 해결하기 위해 등장한 것이 바로 전역 상태 관리입니다. 전역 상태 관리를 사용하면 필요한 상태를 컴포넌트 어디에서나 손쉽게 접근하고 관리할 수 있습니다.

이제, 이런 전역 상태 관리 방식을 간단하고 효과적으로 구현할 수 있는 라이브러리 중 하나인 Zustand를 살펴보겠습니다.


Zustand 기본 개념 🏗️

1. Store (스토어) - 데이터 창고

스토어는 모든 상태와 상태를 변경하는 함수들을 담는 곳입니다.

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 }),
}))

2. set과 get 함수 이해하기

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>
  )
}

로직 흐름 설명 🔄

  1. 스토어 생성: create 함수로 스토어를 만듭니다
  2. 상태 정의: count: 0으로 초기 상태를 설정합니다
  3. 액션 정의: increment, decrement, reset 함수를 만듭니다
  4. 컴포넌트에서 사용: useCounterStore()로 스토어를 가져옵니다
  5. 버튼 클릭: 사용자가 버튼을 클릭하면 해당 액션이 실행됩니다
  6. 상태 업데이트: set 함수로 상태가 변경됩니다
  7. 자동 리렌더링: 상태가 변경되면 해당 상태를 사용하는 컴포넌트가 자동으로 다시 렌더링됩니다

선택적 구독 (Selective Subscription) 🎯

왜 필요한가요?

성능 최적화를 위해서입니다. 컴포넌트가 필요한 상태만 구독하도록 하면, 다른 상태가 변경되어도 불필요한 리렌더링을 방지할 수 있습니다.

전체 스토어 구독 vs 선택적 구독

// ❌ 전체 스토어 구독 (비효율적)
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>
}

비동기 액션 처리 🔄

API 호출과 함께 사용하기

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는 정말 간단하면서도 강력한 상태 관리 라이브러리입니다.

핵심 포인트 정리:

  1. create: 스토어를 만드는 함수
  2. set: 상태를 업데이트하는 함수
  3. get: 현재 상태를 가져오는 함수
  4. 선택적 구독: 성능 최적화를 위해 필요한 상태만 구독
  5. 비동기 처리: API 호출과 함께 사용 가능
profile
풀스택 개발자로서의 도전을 하는 중입니다. 많은 응원 부탁드립니다!!😁

0개의 댓글