sagen - 상태 관리 라이브러리

Yongbeen Im·2021년 1월 31일
10
post-thumbnail

sagen npm

규모가 큰 앱을 개발할 때 props drilling을 피하기 위한 목적으로, 상태가 변경하는 것을 직접 모니터링하기 위한 목적으로 또는 그 외의 이유로 상태 관리 라이브러리를 사용하곤 합니다.

또한, 상태 관리 라이브러리를 사용하지 않고 ContextAPI를 사용하는 경우도 있습니다(이 경우, ContextAPI는 타 상태 관리 라이브러리와 달리 렌더링 최적화가 이루어지지 않음을 인지하고 설계되어야 합니다).

위와 같이 이미 많은 상태 관리 라이브러리가 존재하며, 새로운 라이브러리가 종종 등장합니다. 본 글에서는 상태 관리 라이브러리 중 하나인 sagen에 대해서 알아보도록 하겠습니다.

sagen 상태 관리 라이브러리

sagen 라이브러리는 Provider가 존재하지 않습니다.

Redux는 하나의 store를 제공하며, MobX는 여러 store를 제공합니다.

Redu와 같은 경우 앱의 최상단에 Provider를 위치시켜 모든 위치에서 참조 가능하게 만들면 되지만, MobX와 같이 여러 store를 제공하는 경우 최적의 Provider 위치를 찾아서 입력해야 합니다(이 위치는 페이지를 감싸는 Wrapper 컴포넌트가 될 수 있고, 일부 컴포넌트를 감싸고 있을 수도 있습니다).

ContextAPI를 사용하면 되지 않을까?

React에서 제공하는 상태를 공유하는 방법 중 하나인 Context API에도 한계가 존재합니다.

이는 데이터의 변경이 자주 일어나고 데이터를 참조하는 곳이 많아질 수록 업데이트가 비효율적으로 이뤄지게 됩니다.
https://jungpaeng.tistory.com/58

Context API는 이러한 특성을 갖고 있어 낮은 빈도의 업데이트에 최적화되어 있으며, Flux와 같은 상태 관리 시스템을 대체할 수 없습니다.

이러한 이유로 Redux 라이브러리에서는 Context API를 사용하면서도 이를 최적화하기 위한 로직이 포함되어 있습니다.
https://jungpaeng.tistory.com/72

sagen의 특징에는 무엇이 있을까?

  1. sagen은 바닐라 자바스크립트에서도 원활하게 동작하며, 이를 react에서 사용할 수 있는 라이브러리를 내장하고 있습니다.
  2. sagen은 배우기 쉽습니다. API가 단순하며, 사용될 수 있는 예제가 ReadMe 파일에 나와 있으며, React Hook 기반으로 사용할 수 있어 익숙하게 다가올 것입니다.
  3. 여러 store를 제공하고 있으며, Provider의 위치를 고민하지 않아도 되도록 store 내에서 데이터를 관리하고 있습니다.
  4. selector를 이용해 인자를 보내는 것으로 필요한 데이터만을 가져올 수 있습니다.
  5. 손쉽게 persist를 설정할 수 있습니다.

sagen 하나씩 살펴보기

Store
Store는 상태 값을 관리하는 '공간'입니다. 이 store에 담겨 있는 값 중 일부가 변경되면 이 store 내의 값을 사용하고 있는 모든 컴포넌트들이 다시 렌더링됩니다.

createStore
store를 생성하기 위한 함수입니다. 인자로는 defaultValue를 전달받으며, 이 값으로는 숫자, 문자열, 배열, 객체 등의 값이 될 수 있습니다.

useGlobalStore
생성된 store를 인자로 전달받으며, [state, setState]의 getter, setter를 반환합니다.

이 구조는 useState hook을 사용해본 경험이 있다면 쉽게 적응할 수 있을 것입니다.

state selector
state 값을 가져올 때 selector 함수를 넘겨 state를 가공할 수 있습니다.

기본적으로 store에 저장되어 있는 값이 변경되면, === 연산자를 통해 기존 값과 새로운 값을 비교해 컴포넌트 업데이틀 여부를 결정하므로, 아래와 같이 state에서 필요한 값만을 가져와 사용하는 것이 최적화에 도움이 됩니다.

import React from 'react';
import { createStore, useGlobalStore } from 'sagen';

const globalStore = createStore({ num: 0, str: '' });
const numberSelector = state => state.num;
const stringSelector = state => state.str;

const NumberChild = () => {
  const [num, setValue] = useGlobalStore(globalStore, numberSelector);
  const handleClickBtn = React.useCallback(() => {
    setValue((curr) => ({
      ...curr,
      num: curr.num + 1,
    }));
  }, []);

  return (
    <div className="App">
      <p>number: {num}</p>
      <button onClick={handleClickBtn}>Click</button>
    </div>
  );
};

const StringChild = () => {
  const [str] = useGlobalStore(globalStore, stringSelector);

  return (
    <div className="App">
      <p>string: {str}</p>
    </div>
  );
};

const App = () => {
  return (
    <div>
      <NumberChild />
      <StringChild />
    </div>
  );
};

위와 같은 예시에서 num 값이 변경된다면 str 값만을 사용하고 있는 StringChild 컴포넌트는 업데이트가 일어나지 않게 됩니다.

customSetState
createStore 함수에 (setter: any) => {state: any, customSetState: any} 형태의 함수를 넘겨 반환받는 setter를 커스텀할 수 있습니다.

const testStore = createStore((set) => {
  return {
    state: {
      num: 1,
      str: 'test',
    },
    customSetState: {
      setNum: (num: number) => set((prev: any) => ({ ...prev, num })),
    },
  };
});

const App = () => {
  const [state, setState] = useGlobalStore(testStore);
  const { num, str } = state;
  const { setNum } = setState;

  return (
    <div className="App">
      <p>number state: {num}</p>
      <button onClick={() => setNum(100)}>
        ClickMe
      </button>
    </div>
  );
};

createStore에 함수를 전달할 경우, 첫 번째 인자로 전달받는 setter에서 prev state 값을 가져와 이를 수정할 수 있습니다.

custom setter prev value
useStatesetter에 함수를 전달해 이전 값을 참조할 수 있습니다. 기본적으로 제공해주는 setter를 사용하면 이와 같은 기능을 사용할 수 있지만, customSetter를 사용하는 경우 해당 기능이 제외됩니다.

다음과 같이 작성해 custom setter에 prev 값을 인자로 갖고 있는 형태의 함수를 전달시킬 수 있습니다.

customSetState: {
  setNum: (numFunc) => {
    if (typeof numFunc === 'function') {
      return set((prev: any) => ({ ...prev, num: numFunc(prev.num) }));
    } else {
      return set((prev: any) => ({ ...prev, numFunc }));
    }
  }
}

위의 사용 예시는 다음과 같습니다.

setNum(100);
setNum(prev => prev.num + 100);

shallowEqual
selector를 사용해 사용하는 값을 구분하기 어려운 경우, shallowEqual를 넘겨 비교 연산 자체를 수정할 수 있습니다.

useGlobalStore의 두 번째 인자로 selector 함수가 전달되는 것을 요구하기 때문에 state => state와 같은 형태로 상태 값을 그대로 반환해주는 selector 함수를 넘겨 사용할 수 있습니다.

import React from 'react';
import { createStore, useGlobalStore, shallowEqual } from 'sagen';

const globalStore = createStore({ num: 0, str: '' });
const storeSelector = state => state;

const App = () => {
  const [state, setState] = useGlobalStore(globalStore, storeSelector, shallowEqual);

  return (
    <div>
      ...
    </div>
  );
};

with vanilla js
sagencreateStore는 React에 종속되어 있지 않습니다. createStore를 호출해 바닐라 자바스크립트에서 사용할 수 있습니다.

redux middleware
redux 미들웨어를 사용할 경우, 리듀서 함수를 전달해 값을 관리할 수 있습니다.

// reduxStore.ts
export function testReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
};

const reduxStore = createStore(redux(testReducer, 0));
const App = () => {
  const [state, dispatch] = useGlobalStore(reduxStore);

  return (
    <div className="App">
      <p>state: {state}</p>
      <button
        onClick={() => dispatch({ type: 'INCREMENT' })}
      >
        ClickMe
      </button>
    </div>
  );
}

redux 함수의 첫 번째 함수로 reducer 함수, 두 번째 인자로 defaultValue 값을 전달하면 됩니다.

이 store를 useGlobalStore에 넘길 경우 [state, dispatch]와 같은 형태로 반환합니다. useReducer hook을 사용해본 경험이 있다면 더욱 빠르게 적응할 수 있을 것입니다.

persist middleware
localStorage, sessionStorage 등에 값을 저장하기 위한 persist 미들웨어를 제공합니다.

const globalStore = createStore(
  persist(
    {
      name: 'local-persist-test',
      storage: localStorage,
    },
    redux(testReducer, 0),
  ),
);

사용해볼만한 라이브러리인가?

React에서 제공하는 Hook에 익숙하다면 손쉽게 사용할 수 있는 라이브러리로, 충분히 사용해볼만한 가치가 있다고 생각합니다.

profile
C, C++ 및 웹 프론트 프로그래밍을 즐겨하며, 새로운 것을 배우는 것을 좋아합니다.

1개의 댓글

comment-user-thumbnail
2021년 2월 1일

sagen 라이브러리에 개선/추가되었으면 하는 기능이 있다면 이슈 작성해주세요.
검토 후 빠르게 반영할 수 있도록 하겠습니다.

답글 달기