반응형

- 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
반응형
'웹개발 > reactjs' 카테고리의 다른 글
리액트 + 타입스크립트 기초 2단계: useState & 이벤트 처리 (0) | 2025.04.14 |
---|---|
리액트 + 타입스크립트 기초 1단계: 환경 설정 & 컴포넌트 기본 (0) | 2025.04.14 |
[리액트] React Query를 사용하는 이유 (0) | 2025.03.23 |
[리액트] React18 주요 개선점 (0) | 2025.03.17 |
[리액트] firebase 와 REACT react-router-dom 404 에러 (0) | 2025.01.23 |