Zustand, Redux (feat. Flux Architecture)

미키오·2024년 6월 4일
0
post-thumbnail

0. 들어가며 ..

React는 컴포넌트 기반으로 설계되어, UI를 작은 독립적인 컴포넌트로 나누어 마치 레고를 조립하듯이 개발을 할 수 있다는게 정말 큰 장점이다. 이러한 컴포넌트 기반 설계는 재사용성과 유지보수성을 높여주지만, 단점으로는 컴포넌트 간의 상태 공유와 관리를 복잡하게 만든다.

특히, 상위 컴포넌트에서 하위 컴포넌트로 props를 전달하는 과정에서 props drilling 현상이 발생할 수 있다.

부트캠프에서 리액트를 처음 배웠던 당시 필자가 실제로 겪었던 상황이다.

Main.js 컴포넌트가 장바구니 현황을 나타내는 cart 상태와 addToCart 함수를 productList.js, ProductItem.js, Cart.js를 거쳐 Payment.js로 전달하는 과정을 보여준다. 각 중간 컴포넌트가 필요하지 않은 cartaddToCart를 단순히 하위 컴포넌트로 전달해야 하는 상황을 나타낸다.

이와 같이 Props Drilling은 상태나 함수를 여러 계층의 중간 컴포넌트에도 걸쳐 전달해야 하는 상황을 초래하며, 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만들 수 있다.

당시의 경험을 통해 상태 관리 라이브러리의 필요성을 절실히 느끼게 되어 이후의 프로젝트에서는 대표적인 상태 관리 라이브러리인 Redux를 사용하여 모든 컴포넌트에서 상태를 공유할 수 있도록 구현했다. 그러나 Redux는 설정이 복잡하며, 스토어, 리듀서, 액션, 미들웨어 등을 설정하며 초기 코드 구현에 시간이 오래 걸린다는 것을 체감했다.

새 프로젝트를 위한 여러 상태 관리 라이브러리를 모색하던 중, Redux처럼 Flux 아키텍처를 사용하는 Zustand를 알게 되었다.

A small, fast, and scalable bearbones state management solution.

작고 빠르며 확장 가능한 최소한의 상태 관리 솔루션.

npm trends로 확인해본 결과 2024년 6월 기준 리덕스를 제외한 jotai, mobx, recoil과 같은 다른 상태관리 라이브러리들을 제치고 높은 사용률을 보이고 있다. 이 글에서는 Zustand와 Redux를 비교하고, 각 라이브러리가 어떻게 상태 관리를 돕는지 간단한 코드에 적용해본 경험을 바탕으로 정리해보겠다. 먼저 Flux 아키텍처에 대해 알아보자.

Flux 아키텍처

Flux는 Facebook (Meta) 에서 개발한 애플리케이션 아키텍처로, 데이터의 단방향 흐름을 강조한다. 이를 통해 애플리케이션의 상태를 예측 가능하고 쉽게 디버깅할 수 있다는 장점이 있다. Flux는 다음과 같은 주요 요소로 구성된다.

  • Action: 애플리케이션에서 발생하는 이벤트를 설명하는 객체
  • Dispatcher: 모든 Action을 중앙에서 관리하고, 등록된 콜백을 통해 Action을 Store로 전달
  • Store: 애플리케이션의 상태와 비즈니스 로직을 포함하는 객체. 상태를 관리하고 Action을 받아 상태를 업데이트
  • View: React 컴포넌트로, Store의 상태를 기반으로 UI를 렌더링

즉, 사용자가 View에서 입력을 발생시키면 입력 데이터는 Action을 통해서 Dispatcher를 거쳐 Store로 향하게 된다.

Redux vs Zustand

Redux

Redux는 Flux 아키텍처에 기반을 둔 리액트의 대표적인 상태 관리 라이브러리로, 애플리케이션의 상태를 중앙에서 관리한다. 또한 엄격한 구조와 다양한 미들웨어를 제공하여 확장성과 유지 보수성을 높일 수 있다. 그러나 이러한 강력한 기능에도 불구하고 명확한 단점이 존재한다:

  • 복잡한 설정과 구조: Redux는 설정이 복잡하며, 스토어, 리듀서, 액션, 미들웨어 등을 설정하는 데 많은 시간이 소요됨.
  • 장황한 코드: Redux는 액션과 리듀서를 정의하는 데 많은 코드가 필요합니다. 이는 작은 상태 변경에도 많은 보일러플레이트 코드를 작성해야 함을 의미한다.
  • 높은 학습 곡선: Redux는 개념적으로 복잡하고, 이를 제대로 사용하기 위해서는 Flux 아키텍처와 관련된 깊은 이해가 필요함.
  • 느린 초기 개발 속도: 설정과 코드 작성이 복잡하기 때문에 초기 개발 속도가 느려질 수 있다.
    https://mikio999.github.io/zustandBear/
    https://mikio999.github.io/zustandBear/
    리덕스로 곰돌이의 수를 전역으로 상태 관리하는 곰돌이 생성 웹페이지를 구현한다면 다음과 같은 과정이 필요하다.

Actions

//actions.js
export const INCREASE_POPULATION = 'INCREASE_POPULATION';
export const DECREASE_POPULATION = 'DECREASE_POPULATION';
export const HEXAGONAL_POPULATION = 'HEXAGONAL_POPULATION';
export const REMOVE_ALL_BEARS = 'REMOVE_ALL_BEARS';

export const increasePopulation = () => ({ type: INCREASE_POPULATION });
export const decreasePopulation = () => ({ type: DECREASE_POPULATION });
export const hexagonalPopulation = () => ({ type: HEXAGONAL_POPULATION });
export const removeAllBears = () => ({ type: REMOVE_ALL_BEARS });

Actions를 통해 애플리케이션에서 발생할 수 있는 이벤트를 정의한다. 각 이벤트는 액션 타입으로 정의되며, 이를 사용하여 상태를 변경할 수 있다.

Reducers

import {
  INCREASE_POPULATION,
  DECREASE_POPULATION,
  HEXAGONAL_POPULATION,
  REMOVE_ALL_BEARS
} from './actions';

const initialState = { bears: 0 };

const bearReducer = (state = initialState, action) => {
  switch (action.type) {
    case INCREASE_POPULATION:
      return { ...state, bears: state.bears + 1 };
    case DECREASE_POPULATION:
      return { ...state, bears: state.bears - 1 };
    case HEXAGONAL_POPULATION:
      return { ...state, bears: state.bears + 6 };
    case REMOVE_ALL_BEARS:
      return { ...state, bears: 0 };
    default:
      return state;
  }
};

export default bearReducer;

리듀서는 상태를 어떻게 변경할지를 정의하는 순수 함수이다. 액션 타입에 따라 상태를 업데이트한다.

Store

import { createStore } from 'redux';
import bearReducer from './reducers';

const store = createStore(bearReducer);

export default store;

Redux 스토어를 생성한다. 이는 애플리케이션의 전체 상태 트리를 포함하는 객체 역할을 한다.

App.js, Index.js

Redux와 React를 연결하는 부분으로 useSelector 훅을 사용하여 상태를 선택하고, useDispatch 훅을 사용하여 액션을 디스패치한다.

// App.js
import React from 'react';
import { Provider, useSelector, useDispatch } from 'react-redux';
import {
  increasePopulation,
  decreasePopulation,
  hexagonalPopulation,
  removeAllBears
} from './actions';
import store from './store';

function App() {
  const bears = useSelector((state) => state.bears);
  const dispatch = useDispatch();

  return (
    <div>
      <h1>{bears} 마리</h1>
      <button onClick={() => dispatch(increasePopulation())}>a Bear</button>
      <button onClick={() => dispatch(decreasePopulation())}>Go Home</button>
      <button onClick={() => dispatch(hexagonalPopulation())}>Hexagonal</button>
      <button onClick={() => dispatch(removeAllBears())}>Hibernation</button>
    </div>
  );
}

// Index.js
export default () => (
  <Provider store={store}>
    <App />
  </Provider>
);

또한 Redux를 사용할 때 가장 루트 폴더에 있는 index.js 파일에서 Provider를 설정하여 Redux 스토어를 React 애플리케이션에 연결해야 한다. Provider는 애플리케이션 전체에서 Redux 스토어를 사용할 수 있도록 한다.

이번에는 zustand로 같은 기능을 구현해보자.

Zustand

상태 및 액션 정의

import { create } from "zustand";
import "./App.css";
import Card from "./Card";

export const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  decreasePopulation: () => set((state) => ({ bears: state.bears - 1 })),
  hexagonalPopulation: () => set((state) => ({ bears: state.bears + 6 })),
  removeAllBears: () => set({ bears: 0 }),
}));

Zustand는 create 함수를 사용하여 상태와 상태를 변경하는 액션을 정의한다.

이 예제에서는 bears 상태와 이를 증가시키거나 감소시키는 함수들을 정의한다.

상태 사용 및 액션 디스패치

function App() {
  const bears = useStore((state) => state.bears);
  const increasePopulation = useStore((state) => state.increasePopulation);
  const decreasePopulation = useStore((state) => state.decreasePopulation);
  const hexagonalPopulation = useStore((state) => state.hexagonalPopulation);
  const removeAllBears = useStore((state) => state.removeAllBears);

  return (
    <div className="App">
      <h1>{bears} 마리</h1>
      <button onClick={increasePopulation}>a Bear</button>
      <button onClick={decreasePopulation}>Go Home</button>
      <button onClick={hexagonalPopulation}>Hexagonal</button>
      <button onClick={removeAllBears}>Hibernation</button>
      <Card />
    </div>
  );
}

export default App;

useStore 훅을 사용하여 상태를 읽고, 상태를 변경하는 함수를 가져온다. 이 훅은 상태가 변경될 때마다 컴포넌트를 자동으로 다시 렌더링한다.

같은 기능을 리덕스로 하는 것보다 코드가 훨씬 간결해진다는 것을 알 수 있다!

Redux와 Zustand 비교

공통점

  • Flux 아키텍처 기반의 상태 관리 라이브러리 (Action, Store, Reducer 등을 사용)
  • 컴포넌트가 상태를 구독하여 상태가 변경될 때 자동으로 다시 렌더링

차이점

  • 설정 및 복잡성:
    • Redux: 설정이 복잡하며, 스토어, 리듀서, 액션, 미들웨어 등을 설정하는 데 많은 시간이 소요된다. 이는 대규모 애플리케이션에서 예측 가능성과 확장성을 제공하지만, 초기 러닝커브가 높고 설정이 번거롭다.
    • Zustand: 설정이 간단하며, 단일 파일로 모든 상태 관리를 구성할 수 있다. 코드가 간결하고 사용이 쉬워 작은 프로젝트나 간단한 상태 관리에 적합하다.
  • 상태의 중앙 집중화:
    • Redux: 모든 애플리케이션 상태가 하나의 중앙 저장소(store)에 저장된다. 이는 상태 관리가 용이하고, 애플리케이션의 어느 부분에서든 상태에 접근하거나 변경할 수 있다.
    • Zustand: Zustand에서는 필요에 따라 여러 스토어를 생성할 수 있으며, 각 스토어는 독립적으로 상태를 관리할 수 있다. 작은 단위의 상태를 관리하기에 적합.
  • 타임 트래블 디버깅:
    • Redux: Redux DevTools를 통해 상태 변경을 기록하고 과거의 상태로 돌아가 애플리케이션의 동작을 재현할 수 있다. 디버깅에 매우 유용.
    • Zustand: 타임 트래블 디버깅 기능을 기본적으로 제공하지 않는다.
  • 미들웨어:
    • Redux: 미들웨어를 통해 상태 변경의 과정을 확장할 수 있다. 이를 통해 비동기 작업을 처리하거나 로깅, 충돌 보고 등 다양한 기능을 쉽게 추가할 수 있다.
    • Zustand: 미들웨어 개념이 없지만, 별도의 플러그인 메커니즘을 통해 비동기 작업 등을 처리할 수 있다.
  • 액션과 리듀서:
    • Redux: 상태 변경은 항상 액션과 리듀서에 의해 처리된다. 리듀서는 순수 함수로서, 동일한 입력이 주어지면 항상 동일한 출력을 생성한다.
    • Zustand: 상태 변경은 함수 호출을 통해 직접적으로 처리된다. 액션과 리듀서의 개념이 없으며, 상태 변경 로직이 간단하다.

마치며..

Zustand의 한계?

Zustand는 직관적이고 구조가 간단하여 쉬워보이지만 Zustand만을 사용하기에는 위에서 언급한 것처럼 분명한 한계점들이 존재한다.

  • 비동기 상태 관리의 내장자원 부족
  • 복잡한 상태 관리의 어려움
  • 부족한 미들웨어 미지원 (로깅, 충돌 보고 등)

React Query와의 융합의 장점

React Query는 비동기 데이터 페칭, 캐싱, 동기화, 서버 상태 관리에 특화된 라이브러리로, 이러한 문제를 해결할 수 있다.

Zustand와 React Query를 함께 사용 시의 장점:

  1. 비동기 데이터 페칭 및 캐싱:
    • React Query를 사용하면 서버에서 데이터를 비동기로 쉽게 가져올 수 있으며, 데이터 캐싱을 통해 네트워크 요청을 최소화할 수 있다. 이는 Zustand의 비동기 상태 관리 부족 문제를 해결한다.
  2. 자동 리페치 및 동기화:
    • React Query는 데이터가 변경되거나 필요할 때 자동으로 데이터를 리페치하고 동기화한다. 이는 상태를 항상 최신 상태로 유지하는 데 도움이 된다.
  3. 간단한 API와 유연성:
    • React Query는 간단한 API를 제공하여 비동기 데이터를 관리하기 쉽게 한다. Zustand와 함께 사용하여 전역 상태와 비동기 상태를 분리하고 관리할 수 있다.
  4. 복잡한 상태 관리의 분리:
    • Zustand는 클라이언트 상태(예: UI 상태, 로컬 상태) 관리를 담당하고, React Query는 서버 상태(예: API 응답, 비동기 데이터) 관리를 담당함으로써 역할을 분리할 수 있다. 이를 통해 상태 관리가 명확하고 효율적으로 이루어질 수 있다.

그러므로..

Zustand와 React Query를 함께 사용하는 것은 상태 관리의 복잡성을 줄이고, 비동기 작업을 효율적으로 처리하는 데 용이하다. Zustand는 간단한 로컬 상태 관리를 담당하고(Client), React Query는 비동기 데이터 페칭과 서버 상태 관리를 담당함 (Server)으로써 각자의 강점을 살릴 수 있습니다.

추후 프로젝트에서 이러한 장점을 최대한 활용하기 위해 React Query에 대해 더 깊이 공부할 필요가 있음을 느꼈다. React Query의 다양한 기능과 활용 방법을 익혀서, 비동기 데이터 관리를 더욱 효과적으로 할 수 있는 방법을 구상하고 이를 적절히 Zustand와 잘 활용해야겠다는 생각이 들었다.

📚Bibliography

코딩애플 zustand: https://youtu.be/zNHZJ_iEMPA?si=BHZuTG8Byqj5L2a7
우아콘 2023 : https://youtu.be/nkXIpGjVxWU?si=9bNlenNMTOjYR_4w
zustand 공식문서 : https://zustand-demo.pmnd.rs/
리덕스 공식문서 : https://ko.redux.js.org/
flux 구조 : https://facebookarchive.github.io/flux/docs/in-depth-overview/
본인 깃허브 : https://mikio999.github.io/zustandBear/

profile
교육 전공 개발자 💻

0개의 댓글