전역상태관리 라이브러리 (feat. redux, zustand, valtio)

브리·2023년 9월 12일
1

1️⃣ Redux

Redux 는 장단점만 가볍게 정리하려고 한다.

장점

flux 패턴을 사용함으로 단방향 데이터 흐름을 가져간다. -> 데이터를 예측가능하게 하고 데이터 흐름의 복잡성을 해결한다.
react의 초반 전역 상태 라이브러리의 표준을 잡아주는 역할을 했기 때문에 무시할 수 없는 사용량과 커뮤니티

단점

무게가 무겁다.
복잡한 코드 작성, 기능을 위한 보일러 플레이트가 많아서 러닝커브가 높음

2️⃣ zustand

zustand란 전역 상태 관리 라이브러리로 redux에 비해 상대적으로 간단하고 직관적인 문법을 제공한다. Flux패턴을 사용하여 단방향의 데이터 흐름을 가지고 있어, 변화를 추적하기 용이하고 데이터 흐름의 복잡성을 해결한다. pub/sub 모델을 가지고 있어 느슨한 결합으로 인한 확장성에서 장점을 가지고 있다.

zustand의 코어 로직

zustand의 로직(ts 제외하고 로직만 살펴봄)을 살펴보면 아래와 같은데 state를 클로져를 통해 관리한다. 클로저로 상태를 관리함으로써 가져갈 수 있는 이점은
1. 외부에서의 무분별한 접근과 수정을 막기 때문에 데이터 무결성을 지킬 수 있다.
2. 스코프가 명확하고 순수함수로 이루어져 있기에 의도치 않은 사이드이펙트를 막을 수 있고 예측 가능하게 동작한다.

const createStoreImpl = createState => {
  let state;
  const listeners = new Set();

  const setState = (partial, replace) => {
    const nextState = typeof partial === "function" ? partial(state) : partial;

    if (!Object.is(nextState, state)) {
      const previousState = state;
      state =
        replace ?? typeof nextState !== "object"
          ? nextState
          : Object.assign({}, state, nextState);

      listeners.forEach(listener => listener(state, previousState));
    }
  };

  const getState = () => state;

  const subscribe = listener => {
    // ... (생략)
  };

  const api = { setState, getState, subscribe };
  state = createState(setState, getState, api);

  return api;
};

setState

상태를 변경하는 setState 함수

const setState = (partial, replace) => {
    const nextState = typeof partial === "function" ? partial(state) : partial;

    if (!Object.is(nextState, state)) {
      const previousState = state;
      state =
        replace ?? typeof nextState !== "object"
          ? nextState
          : Object.assign({}, state, nextState);

      listeners.forEach(listener => listener(state, previousState));
    }
  };
  1. partial이 함수일 경우는 함수의 반환값 or 혹은 값을 nextState에 저장한다.
  2. Object.is를 통해 현재 상태와 변경 상태를 비교하여 변경이 일어났을 때 새로운 상태를 state에 할당하고 리스너 함수를 호출하여 상태 변경을 알린다.
  • Object.assign()을 사용한 이유? 불필요한 렌더링을 막고 불변성을 유지하기 위해서이다. 직접적으로 수정하지 않고 새로운 객체를 반환함으로써 불변성을 지킬 수 있기 때문에 사용하였다. 핵심은 불변성이기 때문에 Object.assign({}, state, nextState); 대신 {...state, ...nextState} 를 써도 똑같이 동작하지 않을까 ..??

react에서의 사용

react 18 버전에서 cuncurrent rendering이 지원되면서 외부 스토어(zustand store, DOM 객체 등...)을 참조할 경우에 tearing 문제가 발생할 가능성이 생겼다.

  • tearing이란?
    external store에 접근하여 값을 사용할 경우 특정 조건에서 유저가 느끼는 시각적 불일치 현상. 기존의 동기 렌더링의 경우 한 번 렌더링이 시작하면 멈추지 않기 때문에 시간 지연 문제가 발생할 수는 있으나 모든 컴포넌트가 동일한 external store 의 동일한 값을 가진다. 하지만 18 버전에 concurrent rendering이 도입되면서 렌더링 도중 우선순위가 높은 작업에 양보하기 때문에 렌더링 과정에서 외부스토어의 다른 값을 참조할 가능성이 생기기 때문이다.

이러한 tearing 문제를 해결하기 위헤 useSyncExternalStore라는 긴 이름의 React hook 을 사용한다. 외부 스토어에 대한 싱크로나이제이션을 강제하는 훅으로 알고있다. 해당 훅은 18버전에서만 사용할 수 있다.

zustand & useSyncExternalStore

그렇기 때문에 zustand는 useSyncExternalStore 훅을 사용하여 위와 같은 문제를 해결하고 있다. (코드)
아래는 useStore 함수 안에 있는 useSyncExternalStore의 모습.. 사실 useSyncExternalStore의 사용법을 잘 몰라서 위의 로직과 달리 직관적으로 이해가 안된다ㅠㅠ.

const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn
  )

내가 느낀 바로는 flux 패턴과 pub-sub 모델을 가져가며 데이터에 대한 예측 가능성과 무결성, 내부 코드의 불변성도 가져가고 있고 devTools 지원에도 문제가 없고.. redux 보다 가볍고 러닝커브가 낮은 zustand를 더 높게 평가하고 있다. zustand야 더 힘을 내줘.

3️⃣ valtio

js의 proxy를 사용하여 전역상태를 관리하는 라이브러리이다. 사용방법이 아주~ 직관적이고 간단해서 아주 간단한 개인 프로젝트에도 사용한 라이브러리이다. ssr을 사용하고 서버 컴포넌트를 사용하면서 클라이언트 단의 상태 관리 라이브러리의 경량화를 선호하는데 간단히 쓰기 너무 좋았던 라이브러리이다. 사용해본 사람들 말로는 이와 같이 observable한 상태 관리 라이브러리를 사용할거면 jotai를 더 추천한다고 하긴 함. 왜인지는 나중에 jotai 분석하면서 알아보려 함. (구조화 잘되어있고 다양한 상황에 대응하기 좋다는 이야기를 하심.)

js - proxy?

proxy의 의미는 대리인 라는 뜻인데 말그대로 객체의 기본동작을 대리하여 다른 동작을 할 수 있게 한다는 것이다. 사용방법도 new Proxy(target, handler) 의 방식으로 target은 proxy의 대상객체, handler는 target을 가로채 동작하게 할 함수. Get, set, has 등등이 있당.

변형 감지 : [set]

변형 감지를 위해 set Handler를 사용하고 이러한 객체 변형의 추적을 위해 version number를 사용한다.

//생성
const p = new Proxy({}, {
  set(target, prop, value) {
    ++version;
    target[prop] = value;
  },
});

//라이브러리 로직 내의 version 이 있는 것을 확인할 수 있다.
type CreateSnapshot = <T extends object>(
  target: T,
  version: number,
  handlePromise?: HandlePromise
) => T

snapshot 만들기

snapshot? 변경 불가능한 상태. 스냅샷의 기본은 개체를 복사하는것.

const snapshot = () => {
  if (lastVersion !== version) {
    lastVersion = version;
    lastSnapshot = { ...p }; //여기서 p는 위의 코드 내 p 값
  }
  return lastSnapshot;
};
profile
무럭무럭

0개의 댓글