리액트 쿼리 + 상태관리 라이브러리(zustand? RTK?)를 고민하며 먼저 어떤 상태가 있는지 정의하고 관찰해보겠다.
서버 상태와 클라이언트 상태는 웹 애플리케이션에서 데이터를 관리하는 두 가지 다른 개념이다. 각각의 개념은 역할과 책임이 다르며, 서로 다른 용도와 특징을 가지고 있다.
예시를 통해 서버 상태와 클라이언트 상태의 차이를 살펴보겠다.
서버 상태와 클라이언트 상태는 서로 다른 목적과 역할을 가지고 있으며, 애플리케이션의 특정 요구에 따라 적절하게 사용되어야 한다!
사실 이 2가지 조합을 보려고 한다.
// Zustand (클라이언트 상태 관리)
import create from 'zustand';
interface AuthState {
isAuthenticated: boolean;
user: { username: string } | null;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
}
const useAuthStore = create<AuthState>((set) => ({
isAuthenticated: false,
user: null,
login: async (username, password) => {
// 서버 요청을 통해 인증 처리
try {
// 인증 성공한 경우
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
if (response.ok) {
const user = await response.json();
set({ isAuthenticated: true, user });
} else {
throw new Error('Login failed');
}
} catch (error) {
console.error(error);
}
},
logout: () => {
set({ isAuthenticated: false, user: null });
},
}));
// React Query (서버 상태 관리)
// Presenter
import { useMutation } from 'react-query';
interface LoginResponse {
token: string;
}
async function loginUser({ username, password }: { username: string, password: string }): Promise<LoginResponse> {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
return response.json();
}
function useLoginMutation() {
return useMutation<LoginResponse, Error, { username: string, password: string }>(loginUser);
}
// View: 컴포넌트 예시
import { useState } from 'react';
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const auth = useAuthStore();
const loginMutation = useLoginMutation();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
loginMutation.mutate({ username, password });
};
if (auth.isAuthenticated) {
return <p>Welcome, {auth.user?.username}!</p>;
}
return (
<form onSubmit={handleSubmit}>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit" disabled={loginMutation.isLoading}>Login</button>
{loginMutation.isError && <p>Login failed</p>}
</form>
);
}
위의 코드 예시에서 useAuthStore
함수는 Zustand를 사용하여 클라이언트 상태를 관리합니다. isAuthenticated
는 사용자가 인증되었는지 여부를 나타내고, user
는 현재 인증된 사용자 정보를 저장합니다. login
함수는 사용자 로그인을 처리하고, logout
함수는 로그아웃을 처리합니다.
useLoginMutation
함수는 React Query의 useMutation
훅을 사용하여 서버 요청을 처리하는데 사용됩니다. loginUser
함수는 실제 서버 요청을 보내는 로직을 담당하며, 해당 함수를 useLoginMutation
훅에 전달하여 사용자 로그인 요청을 처리합니다.
위의 코드 예시에서 LoginForm
컴포넌트는 로그인 폼을 나타내며, Zustand와 React Query를 사용하여 클라이언트 상태와 서버 상태를 처리합니다. 사용자가 로그인되어 있는 경우에는 환영 메시지를 표시하고, 그렇지 않은 경우에는 로그인 폼을 렌더링합니다. 로그인 폼에서는 입력된 사용자 이름과 비밀번호를 사용하여 로그인 요청을 보내고, 요청이 진행 중인 동안 버튼을 비활성화합니다. 로그인 요청이 실패한 경우에는 에러 메시지를 표시합니다.
이 코드 예시는 Zustand를 사용하여 클라이언트 상태를 관리하고, React Query를 사용하여 서버 상태를 처리하는 방법을 보여줍니다. 이를 기반으로 웹 애플리케이션에서 사용자 로그인 기능을 구현할 수 있습니다.
장바구니 예시를 들어 사용 코드를 보자.
// Zustand (클라이언트 상태 관리)
import create from 'zustand';
const useCartStore = create((set) => ({
cartItems: [],
addToCart: (item) =>
set((state) => ({ cartItems: [...state.cartItems, item] })),
removeFromCart: (itemId) =>
set((state) => ({
cartItems: state.cartItems.filter((item) => item.id !== itemId),
})),
}));
function Cart() {
const cartItems = useCartStore((state) => state.cartItems);
const addToCart = useCartStore((state) => state.addToCart);
const removeFromCart = useCartStore((state) => state.removeFromCart);
return (
<div>
<h2>장바구니</h2>
<ul>
{cartItems.map((item) => (
<li key={item.id}>
{item.name} - {item.price}원{' '}
<button onClick={() => removeFromCart(item.id)}>제거</button>
</li>
))}
</ul>
<button onClick={() => addToCart({ id: 1, name: '상품', price: 1000 })}>
장바구니에 추가
</button>
</div>
);
}
useCartStore
를 생성하고 클라이언트의 장바구니 상태를 관리 addToCart
함수는 상품을 장바구니에 추가removeFromCart
함수는 장바구니에서 상품을 제거이렇게 Zustand를 사용하여 클라이언트 상태를 관리하면 장바구니 상태를 빠르고 간단하게 업데이트할 수 있다.
// React Query (서버 상태 관리)
import { useQuery } from 'react-query';
function GetCartItem() {
const { data: products, isLoading, isError } = useQuery('products', () =>
fetch('/api/products').then((response) => response.json())
);
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error fetching products</div>;
}
return (
<div>
<h2>상품 목록</h2>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} - {product.price}원
</li>
))}
</ul>
</div>
);
}
useQuery
의 첫 번째 매개변수는 쿼리 식별자로 사용되고, 두 번째 매개변수는 데이터를 가져오는 비동기 함수로 네트워크 호출.product
리스트 Return이를 통해 서버 상태를 관리하면 데이터를 가져오고 업데이트할 수 있다.
웹프론트 해본지 3년은 된 것 같은데 요새는 redux보다 zustand를 더 많이 쓰나요?