조아마시

쓸모 있는 상세페이지 만들기

웹개발/reactjs

[리액트] React Query와 Zustand 알아보기

joamashi 2025. 3. 23. 19:42
반응형

  • React Query: **서버 상태(Server State)**를 관리하는 도구
  • Zustand: **클라이언트 상태(Client State)**를 관리하는 전역 상태 관리 도구

핵심 차이 요약

주 용도 서버 데이터 캐싱 및 비동기 처리 전역 상태(클라이언트 로컬 상태) 관리
상태 저장 위치 React Query 내부 캐시 (비휘발성) 메모리 내 저장 (휘발성)
주요 기능 데이터 페칭, 캐싱, 리페치, 오류 처리 글로벌 스토어, 상태 간단 공유
비동기 처리 내장 지원 (useQuery, useMutation 등) 직접 fetch나 axios 사용 필요
상태 공유 방식 내부 Hook (useQuery 등) create로 만든 스토어 Hook 사용

사용 시나리오 예시

1. React Query 예시: 서버에서 유저 목록을 가져오는 경우

// App.tsx
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'

const fetchUsers = async () => {
  const { data } = await axios.get('/api/users')
  return data
}

export default function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error!</div>

  return (
    <ul>
      {data.map((user: any) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

2. Zustand 예시: 테마 상태나 로그인 상태 관리 등 클라이언트 로컬 상태

// store/useThemeStore.ts
import { create } from 'zustand'

type ThemeState = {
  darkMode: boolean
  toggleDarkMode: () => void
}

export const useThemeStore = create<ThemeState>((set) => ({
  darkMode: false,
  toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
}))
// App.tsx
import { useThemeStore } from './store/useThemeStore'

export default function ThemeToggle() {
  const { darkMode, toggleDarkMode } = useThemeStore()

  return (
    <button onClick={toggleDarkMode}>
      {darkMode ? '다크 모드 끄기' : '다크 모드 켜기'}
    </button>
  )
}

결론: 언제 어떤 걸 써야 할까?

서버 API 요청/캐싱 React Query
사용자 UI 상태(토글, 입력값, 모달 등) Zustand
로그인 상태, 장바구니 등 앱 전역 공유 상태 Zustand
데이터 동기화, 자동 리페치 등 비동기 관리 React Query

React Query와 Zustand는 서로 보완적인 도구로, 같이 사용하는 경우가 많습니다.

React Query와 Zustand를 조합해서 사용하는 실전 예제

시나리오

React Query로 서버에서 유저 리스트를 가져오고,
Zustand로 선택된 유저를 전역 상태로 관리하는 구조입니다.


폴더 구조 (예시)

src/
├── api/
│   └── users.ts             // React Query용 API
├── store/
│   └── useSelectedUser.ts   // Zustand 스토어
├── components/
│   └── UserList.tsx         // 유저 리스트
│   └── UserDetail.tsx       // 유저 상세 보기
├── App.tsx

1. React Query: 유저 데이터 API

// api/users.ts
import axios from 'axios'

export const fetchUsers = async () => {
  const { data } = await axios.get('/api/users')
  return data
}

2. Zustand: 선택된 유저 상태 관리

// store/useSelectedUser.ts
import { create } from 'zustand'

type User = { id: number; name: string; email: string }

type SelectedUserStore = {
  selectedUser: User | null
  setSelectedUser: (user: User) => void
  clearSelectedUser: () => void
}

export const useSelectedUser = create<SelectedUserStore>((set) => ({
  selectedUser: null,
  setSelectedUser: (user) => set({ selectedUser: user }),
  clearSelectedUser: () => set({ selectedUser: null }),
}))

3. 컴포넌트: 유저 리스트 (React Query + Zustand 연동)

// components/UserList.tsx
import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from '../api/users'
import { useSelectedUser } from '../store/useSelectedUser'

export default function UserList() {
  const { data, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  const setSelectedUser = useSelectedUser((state) => state.setSelectedUser)

  if (isLoading) return <div>Loading...</div>

  return (
    <ul>
      {data.map((user: any) => (
        <li key={user.id} onClick={() => setSelectedUser(user)}>
          {user.name}
        </li>
      ))}
    </ul>
  )
}

4. 컴포넌트: 유저 상세 보기 (Zustand로 전역 상태 사용)

// components/UserDetail.tsx
import { useSelectedUser } from '../store/useSelectedUser'

export default function UserDetail() {
  const { selectedUser, clearSelectedUser } = useSelectedUser()

  if (!selectedUser) return <div>유저를 선택해주세요.</div>

  return (
    <div>
      <h2>{selectedUser.name}</h2>
      <p>Email: {selectedUser.email}</p>
      <button onClick={clearSelectedUser}>선택 해제</button>
    </div>
  )
}

5. App.tsx

import UserList from './components/UserList'
import UserDetail from './components/UserDetail'

function App() {
  return (
    <div style={{ display: 'flex', gap: '40px' }}>
      <div>
        <h1>유저 목록</h1>
        <UserList />
      </div>
      <div>
        <h1>유저 상세</h1>
        <UserDetail />
      </div>
    </div>
  )
}

export default App

핵심 요약

  • React Query는 서버 데이터를 가져오고 캐싱
  • Zustand는 선택된 유저를 전역으로 관리하여 다른 컴포넌트에서도 접근 가능

이 구조는 상태를 분리하여 관리하므로 유지보수성, 확장성이 매우 좋습니다.

 

로그인 + 상품 목록 + 장바구니 관리 기능을 갖춘 간단한 전자상거래 예제

시나리오 요약

  • React Query
    • 서버에서 상품 리스트 가져오기
    • 로그인 요청 처리 (mutation)
  • Zustand
    • 로그인된 사용자 정보 전역 관리
    • 장바구니 전역 상태 관리

주요 기능 구성

기능 도구 설명

로그인 React Query + Zustand 로그인 시 서버 요청 후 사용자 상태 저장
상품 목록 React Query 서버에서 상품 리스트 가져오기
장바구니 담기 Zustand 전역 상태로 장바구니 관리
장바구니 목록 보기 Zustand 장바구니 상태 기반 렌더링

1. 로그인 상태 스토어 (Zustand)

// store/useAuthStore.ts
import { create } from 'zustand'

type User = { id: number; username: string }

type AuthState = {
  user: User | null
  setUser: (user: User) => void
  logout: () => void
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
}))

2. 장바구니 스토어 (Zustand)

// store/useCartStore.ts
import { create } from 'zustand'

type Product = { id: number; name: string; price: number }

type CartState = {
  items: Product[]
  addToCart: (product: Product) => void
  removeFromCart: (id: number) => void
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  addToCart: (product) => set((state) => ({
    items: [...state.items, product],
  })),
  removeFromCart: (id) => set((state) => ({
    items: state.items.filter((item) => item.id !== id),
  })),
}))

3. 로그인 컴포넌트

// components/Login.tsx
import { useMutation } from '@tanstack/react-query'
import { useAuthStore } from '../store/useAuthStore'
import axios from 'axios'
import { useState } from 'react'

export default function Login() {
  const setUser = useAuthStore((state) => state.setUser)
  const [username, setUsername] = useState('')

  const mutation = useMutation({
    mutationFn: async (username: string) => {
      const { data } = await axios.post('/api/login', { username })
      return data
    },
    onSuccess: (data) => {
      setUser(data.user)
    },
  })

  const handleLogin = () => {
    mutation.mutate(username)
  }

  return (
    <div>
      <input value={username} onChange={(e) => setUsername(e.target.value)} />
      <button onClick={handleLogin}>로그인</button>
    </div>
  )
}

4. 상품 목록 컴포넌트

// components/ProductList.tsx
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { useCartStore } from '../store/useCartStore'

const fetchProducts = async () => {
  const { data } = await axios.get('/api/products')
  return data
}

export default function ProductList() {
  const { data, isLoading } = useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
  })

  const addToCart = useCartStore((state) => state.addToCart)

  if (isLoading) return <div>Loading...</div>

  return (
    <ul>
      {data.map((p: any) => (
        <li key={p.id}>
          {p.name} - {p.price}원
          <button onClick={() => addToCart(p)}>장바구니</button>
        </li>
      ))}
    </ul>
  )
}

5. 장바구니 컴포넌트

// components/Cart.tsx
import { useCartStore } from '../store/useCartStore'

export default function Cart() {
  const { items, removeFromCart } = useCartStore()

  return (
    <div>
      <h3>장바구니</h3>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} - {item.price}원
            <button onClick={() => removeFromCart(item.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

6. App.tsx 통합

import Login from './components/Login'
import ProductList from './components/ProductList'
import Cart from './components/Cart'
import { useAuthStore } from './store/useAuthStore'

export default function App() {
  const user = useAuthStore((state) => state.user)

  return (
    <div style={{ padding: 20 }}>
      {!user ? (
        <Login />
      ) : (
        <>
          <h2>안녕하세요, {user.username}님</h2>
          <ProductList />
          <Cart />
        </>
      )}
    </div>
  )
}

마무리 요약

  • React Query: 서버 요청 처리 (상품 목록, 로그인 등)
  • Zustand: 사용자 인증 상태, 장바구니 등 전역 상태 관리
  • 기능이 명확히 분리되어 유지보수성과 재사용성이 높음
728x90
반응형