서버 상태 ❤️ 클라이언트 상태

윤뿔소·2023년 6월 29일
5

현업 관련 경험

목록 보기
1/4
post-thumbnail

리액트 쿼리 + 상태관리 라이브러리(zustand? RTK?)를 고민하며 먼저 어떤 상태가 있는지 정의하고 관찰해보겠다.

서버 상태 vs 클라이언트 상태

서버 상태와 클라이언트 상태는 웹 애플리케이션에서 데이터를 관리하는 두 가지 다른 개념이다. 각각의 개념은 역할과 책임이 다르며, 서로 다른 용도와 특징을 가지고 있다.

서버 상태(Server State)

  • 서버 상태는 웹 애플리케이션의 백엔드에 저장되어 있는 데이터를 나타냄.
  • 서버 상태는 데이터베이스, 파일 시스템, 캐시 등에 저장되어 관리.
  • 다중 사용자 환경에서 공유되며, 모든 클라이언트에게 동일한 데이터를 제공.
  • 주로 사용자 정보, 게시물, 상품 정보 등과 같은 영속적인 데이터를 저장.
  • 서버 상태는 보안과 데이터 무결성을 유지하는 중앙 집중식 데이터 소스로 사용.
  • 클라이언트에서 요청이 있을 때 서버로부터 데이터를 가져와 업데이트하거나 서버에 변경사항을 전송.
  • 비동기 상태를 거쳐 가져옴.

클라이언트 상태(Client State)

  • 클라이언트 상태는 웹 애플리케이션의 프론트엔드에서 관리되는 데이터를 의미.
  • 클라이언트 상태는 사용자의 브라우저 또는 애플리케이션 내에 저장되어 관리.
  • 단일 사용자 환경에서만 유효하며, 클라이언트 간에 데이터 공유가 이루어지지 않음.
  • 주로 UI 상태, 사용자 입력, 임시 데이터 등을 저장.
  • 클라이언트 상태는 사용자 경험 개선을 위해 빠른 응답성과 미리 가져온 데이터를 제공.
  • 클라이언트에서 상태 변경이 있을 때 서버로 데이터를 전송하거나, 서버로부터 새로운 데이터를 가져와 업데이트.

예시

예시를 통해 서버 상태와 클라이언트 상태의 차이를 살펴보겠다.

1. 사용자 로그인

  • 서버 상태: 사용자의 인증 정보 (예: 사용자 이름, 암호화된 비밀번호)는 서버 상태로 저장. 서버는 인증을 처리하고 세션 또는 토큰을 생성하여 클라이언트에 제공.
  • 클라이언트 상태: 로그인 후 클라이언트는 세션 또는 토큰을 사용하여 사용자의 인증 상태를 유지. 이 상태는 클라이언트의 로그인 상태를 추적하고, 필요한 경우 서버에 추가 요청을 보내 인증된 요청을 처리.

2. 장바구니

  • 서버 상태: 장바구니에 있는 상품 목록과 수량은 서버 상태로 저장. 사용자의 장바구니 정보는 서버에서 관리되고 유지.
  • 클라이언트 상태: 클라이언트는 장바구니 상태를 유지. 사용자가 상품을 추가하거나 제거할 때 클라이언트 상태를 업데이트하고, 필요한 경우 서버에 변경 사항을 전송하여 장바구니를 동기화.

3. 사용자 환경 설정

  • 서버 상태: 사용자의 환경 설정 (예: 언어, 테마, 알림 설정)은 서버에 저장. 서버는 사용자의 환경 설정을 관리하고 필요한 경우 클라이언트에게 제공.
  • 클라이언트 상태: 클라이언트는 사용자의 환경 설정을 클라이언트 상태로 유지. 사용자가 환경 설정을 변경할 때 클라이언트 상태를 업데이트하고, 필요한 경우 서버에 변경 사항을 전송하여 환경 설정을 동기화.

서버 상태와 클라이언트 상태는 서로 다른 목적과 역할을 가지고 있으며, 애플리케이션의 특정 요구에 따라 적절하게 사용되어야 한다!

React Query + Zustand

사실 이 2가지 조합을 보려고 한다.

1. 로그인

// 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를 사용하여 서버 상태를 처리하는 방법을 보여줍니다. 이를 기반으로 웹 애플리케이션에서 사용자 로그인 기능을 구현할 수 있습니다.

2. 장바구니

장바구니 예시를 들어 사용 코드를 보자.

Zustand: 장바구니 전역 상태 관리하기

// 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>
  );
}
  1. Zustand 라이브러리를 사용하여 useCartStore를 생성하고 클라이언트의 장바구니 상태를 관리
  2. addToCart 함수는 상품을 장바구니에 추가
  3. removeFromCart 함수는 장바구니에서 상품을 제거

이렇게 Zustand를 사용하여 클라이언트 상태를 관리하면 장바구니 상태를 빠르고 간단하게 업데이트할 수 있다.

React Query: 물품 리스트 가져오기

// 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>
  );
}
  1. React Query를 사용하여 useQuery 훅을 이용해 서버에서 상품 목록을 가져옴.
  2. useQuery의 첫 번째 매개변수는 쿼리 식별자로 사용되고, 두 번째 매개변수는 데이터를 가져오는 비동기 함수로 네트워크 호출.
  3. 가져온 상태에 따라 조건 분기 생성 및 product 리스트 Return

이를 통해 서버 상태를 관리하면 데이터를 가져오고 업데이트할 수 있다.

profile
코뿔소처럼 저돌적으로

2개의 댓글

comment-user-thumbnail
2024년 5월 29일

웹프론트 해본지 3년은 된 것 같은데 요새는 redux보다 zustand를 더 많이 쓰나요?

1개의 답글