menulogo

상태 관리 라이브러리

@corinthioniaSeptember 26, 2024

React에서 상태관리가 중요한 이유

Vanilla JavaScript로 개발할 때에는 주로 사용자 이벤트를 처리하고 DOM을 조작하는 데에 관심을 두었습니다.

하지만 React에서는 DOM 조작을 추상화하여 상태관리에 더 많은 관심을 두고 있습니다. React는 상태 변화에 따라 UI를 자동으로 업데이트 하기 때문에, 개발자의 관심사는 "상태"가 어떻게 변화하고 그것이 어떻게 UI에 반영되는지로 자연스럽게 옮겨갔습니다.


MVC 패턴

MVC 패턴이란, Model에 데이터를 저장하고, Controller로 Model의 데이터를 관리하며, View를 통해 사용자에게 보여주는 아키텍처를 의미합니다.

이때 View에서도 사용자가 입력 등의 행위를 통해 Model을 업데이트 할 수 있습니다. 이를 양방향 데이터 흐름이라고 부릅니다.

하지만 이러한 양방향 데이터 흐름은 애플리케이션의 규모가 커질수록 복잡성이 증가하고, 데이터 흐름 추적을 어렵게 하는 문제가 있습니다.

Flux 패턴

위와같이 MVC 패턴에서 발생하는 문제를 해결하기 위해 2014년 페이스북에서 Flux 패턴이라는 새로운 아키텍처를 제안했습니다.

Flux 패턴은 사용자의 입력을 기반으로 Action을 생성하고, 이를 Dispatcher에 전달하여 Store의 데이터를 변경한 뒤 View에 반영하는 단방향 데이터 흐름을 가집니다.

데이터가 단방향으로만 전달되기 때문에 데이터 흐름을 파악하기 용이하고 그 결과를 쉽게 예측할 수 있습니다.


상태관리 라이브러리

전역 상태 관리 라이브러리를 사용할 때의 장단점은 아래와 같습니다.

장점

  • Props drilling을 해결합니다.
  • 여러 컴포넌트에서 같은 상태를 관리할 경우 일관성을 유지하지 힘들지만, 전역 상태관리 라이브러리를 사용한다면 이를 해결할 수 있습니다.
  • 대부분의 상태관리 라이브러리는 비동기적으로 데이터를 변경할 수 있는 도구를 지원합니다.

단점

  • 작은 규모의 프로젝트에서는 오히려 불필요한 복잡성을 추가할 수 있습니다.
  • 컴포넌트가 unmount될 때, 메모리 누수나 성능 저하를 방지하기 위해 구독을 해제하거나 리소스를 정리하는 등의 추가적인 작업이 필요합니다.
  • 컴포넌트의 관심사가 전역상태 클린업까지 확장됩니다 - 컴포넌트가 단순한 UI 표현을 넘어서, 전역 상태 관리와 관련된 책임까지 떠안게 됩니다

상태관리 라이브러리 역시 꼭 필요한 경우에만 사용하는 게 좋습니다.
가장 좋은 상태 관리는 관리할 상태가 없는 것이다


Redux

위에서 언급한 Flux 패턴에 Reducer를 결합하여 만든 것이 Redux입니다.

Redux는 애플리케이션의 모든 상태를 하나의 객체(Store)에서 관리합니다.
비동기 작업 및 사이드 이펙트를 처리하기 위한 redux-thunk redux-saga와 같은 미들웨어가 존재합니다.

Redux를 사용한다면 Flux 패턴에 따라 상태변화가 엄격하게 관리되므로 예측이 가능해집니다.

하지만 Redux는 React 라이브러리가 아니기 때문에 React 내부 스케줄러에 접근할 수 없었습니다. 그래서 동시성 모드(Concurrent mode)가 등장했을 때 사용이 어려워지는 문제가 있습니다. 또한, Store를 구성하기 위해 많은 보일러 플레이트 코드와 장황한 코드를 작성해야 하여 최근에는 잘 사용하지 않는 추세입니다.

Recoil

페이스북(메타)에서 개발한 상태관리 라이브러리로, React 문법에 친화적인 라이브러리입니다. React의 상태처럼 간단한 get/set 인터페이스로 사용할 수 있고 보일러플레이트가 없는 API를 제공합니다.

Recoil에서는 상태를 atom이라는 단위로 관리하고, 컴포넌트는 이 상태를 업데이트하고 구독할 수 있습니다. 또한 selector를 이용하여 파생된 상태를 계산하고 이를 곧바로 UI에 사용할 수 있습니다.

하지만 Recoil도 단점이 존재합니다. 전역 상태를 아톰으로 관리하고, 어느 컴포넌트에서도 바로 아톰을 구독하여 업데이트를 받다 보니 아톰이 여러 군데에서 사용되면 사이드 이펙트가 발생할 수 있습니다. 또한, 메모리 누수 문제와 더불어 SSR에서의 지원이 미비하다는 문제점도 존재합니다. 자세한 사항은 아래 링크를 통해 확인할 수 있습니다!
Recoil, 이제는 떠나 보낼 시간이다

Zustand


Zustand는 간결한 Flux 패턴을 바탕으로 빠르게 확장 가능한 상태관리 라이브러리입니다.

Zustand는 발생/구독(pub/sub) 모델을 기반으로 이루어져 있습니다. 스토어의 상태 변경이 일어날 때 실행할 리스너 함수를 모아 두었다가(sub) 상태가 변경되면 등록된 리스너에게 이를 알립니다.(pub)

사용 방법은 다음과 같습니다.
스토어를 만들고 그 안에 원시 타입, 객체, 함수 등을 넣습니다.

import {create} from 'zustand'

const useBearStore = create((set) ({
		bears: 0,
		increasePopulation: () set((state) ({ bears: state.bears + 1})),
		removeAllBears: () set({ bears: 0 }),
})

그 다음 컴포넌트와 바인딩 합니다.

function BearCounter() {
		const bears = useBearStore((state) state.bears)
		return <h1>{bears}</h1>
}

Recoil을 사용할 때에는 setXXX 형태로 아톰을 변경하는 로직을 컴포넌트 안에서 처리하거나 Custom hook으로 처리하는데, Zustand는 Store 내부에서 바로 처리할 수 있어 편리합니다.

Zustand는 설계적으로 Top-down 방식으로 전역상태를 접근하기 때문에 리렌더링이 자주 발생할 수 있어, 성능이 중요한 애플리케이션에서는 적합하지 않을 수 있습니다.


번외

Context API

Context API는 상태관리 도구가 아니라 의존성 주입 수단입니다.
React Context for dependency injection not state management

Context는 데이터를 전달하는 메커니즘일 뿐이며, 상태를 "관리"하지는 않습니다.

Context는 값을 React 컴포넌트 트리의 특정 부분에 전달하고자 할 때, 그 값을 여러 컴포넌트를 통해 props로 하나하나 전달하지 않고 접근할 수 있도록 하고 싶을 때 Context를 사용하면 됩니다.

또한, Context API는 상태 업데이트 방법을 제공하지 않습니다.
Context의 상태를 바꾸는 방법은 Provider의 value prop을 변경하는 방법뿐입니다. Context API는 Provider를 통해 값을 주입하면 useContext를 통해 해당 값을 구독하는 형태입니다. 즉, Context API는 의존성 주입 수단에 불과합니다.

// Context 생성
const MyContext = createContext();

// Provider 컴포넌트
const MyProvider = ({ children }) => {
  const [value, setValue] = useState('초기 값');

  // Provider의 value prop으로 상태와 상태 변경 함수를 제공
  return <MyContext.Provider value={{ value, setValue }}>{children}</MyContext.Provider>;
};

// Consumer 컴포넌트
const MyComponent = () => {
  const { value, setValue } = React.useContext(MyContext);

  return (
    <div>
      <p>{value}</p>
      <button onClick={() => setValue('새로운 값')}>값 변경</button>
    </div>
  );
};

Context API를 전역 상태관리 라이브러리로 사용한다면, 리렌더링으로 인한 성능적 단점이 있습니다.
Context API의 Provider에 전달되는 value prop이 변경되면, 해당 Provider의 모든 자식 컴포넌트가 리렌더링됩니다. 즉, value가 변경되지 않더라도 자식 컴포넌트가 상태를 구독하고 있다면, 매번 리렌더링이 발생할 수 있습니다.
또한 Provider의 value prop이 객체 형태라면, 해당 객체의 속성이 하나라도 변경되면 자식 컴포넌트는 리렌더링됩니다. 이는 특히 큰 컴포넌트 트리에서 성능 저하로 이어질 수 있습니다.

React query (Tanstack query)

위에서 언급한 상태관리 라이브러리는 Client단의 상태를 관리하기 위한 도구라면, React query는 서버로부터 받아오는 데이터(비동기 상태)를 관리하기 위해 사용하는 도구입니다. 서버의 데이터가 자주 변경되는 경우, 애플리케이션에서 이를 명확하게 반영하는 것이 중요하기 때문에 캐싱, 동기화, 업데이트 등을 지원하는 도구입니다.


요약

이름설명
Redux엄격한 구조로 예측 가능성이 높지만, 보일러플레이트 코드가 많고 복잡
MobX자동 상태 관리와 간단한 코드가 장점이지만, 대규모 프로젝트에서 관리가 어려울 수 있음
RecoilReact에 최적화된 간단한 상태 관리 도구지만, 메모리 누수나 업데이트가 불확실한 문제가 있음
Zustand간결함과 빠른 성능이 장점이나, 대규모 프로젝트에서는 관리가 어려울 수 있음
Jotai간단하고 모듈화된 상태 관리를 제공하지만, 새로움에 따른 생태계 부족이 단점

Reference

← 이전 글CSS / JavaScript 애니메이션
다음 글 →CORS에 대해 알아보기