Project/JavaScript Project

[React] 전역 상태 관리 라이브러리 제작기 (3)

Paeng 2020. 12. 2. 22:56
728x90

[React] 전역 상태 관리 라이브러리 제작기 (3)

React

[React] 전역 상태 관리 라이브러리 제작기 (1)
[React] 전역 상태 관리 라이브러리 제작기 (2)

위 포스트에서 작업한 전역 상태 관리 라이브러리에 useReducer와 같이 reducer 함수를 전달받아서 dispatch action으로 상태를 관리하는 구조를 추가하도록 하겠습니다.

Redux의 상태관리

Redux는 아래와 같이 작성해서 상태 관리를 할 수 있습니다.

import { createStore } from 'redux'

function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
}

const store = createStore(counterReducer)

store.subscribe(() => console.log(store.getState()))
store.dispatch({ type: 'counter/incremented' }) // {value: 1}
store.dispatch({ type: 'counter/incremented' }) // {value: 2}
store.dispatch({ type: 'counter/decremented' }) // {value: 1}

Redux의 구조와 같이 store를 생성할 때 reducer 함수를 인자로 전달하도록 작성할 것입니다.

reducer 함수를 사용해 store를 생성했다면, useGlobalStore Hook의 반환 값으로 dispatch를 반환하도록 할 것입니다.

redux 함수

'redux와 비슷한 구조를 가질 수 있도록 어떻게 해야 할까?' 하는 고민을 정말 많이 했습니다. 구현하는 것은 간단히 했지만, 이렇게 작업하는 것이 정말 효율적인 방법일까? 하는 의문이 들어서 수 차례 수정을 하게 되었네요.

결과적으로는 reducer 함수를 인자로 전달받으면 statedispatch를 반환하는 함수를 반환하는 redux 함수를 작성하였습니다.

글로 적으니까 뭔가 말 장난처럼 보이는데, 코드를 먼저 살펴보겠습니다.

타입

interface ReducerAction {
  type: string;
  [key: string]: any;
}

interface ReducerStore<T> {
  state: T;
  dispatch: DispatchType;
}


export type DispatchType = (action: ReducerAction) => void;
export type ReducerFunction<T> = (state: T, action: ReducerAction) => T;
export type ReducerReturnType<T> = (getState: StoreGetState<T>, setState: StoreSetState<T>) => ReducerStore<T>;

위 타입이 redux 함수에서 사용될 타입입니다.

dispatch에서 action은 객체로 이뤄져 있으며 type 키는 필수로 포함되어야 합니다. type 이외의 정보를 dispatch로 전달해야 할 수 있어서 이를 대응하기 위해 [key: string]: any; 타입을 추가하였습니다.

ReduxStoreredux 함수가 반환하는 함수에서 반환하는 타입을 의미합니다.

redux 함수에서는 ReducerReturnType 타입에서 알 수 있듯이 (getState: StoreGetState<T>, setState: StoreSetState<T>) => ReducerStore<T> 타입을 반환하며, createStore 함수에서 getStatesetState 정보를 넘겨줘서 statedispatch 정보를 얻습니다.

state를 넘겨주는 이유는 redux에서 전달받은 값으로 createStorestate에 값을 저장하기 위함입니다.

실제 구현부

const redux = <T = any>(reducer: ReducerFunction<T>, defaultState: T): ReducerReturnType<T> => (
  getState,
  setState,
): ReducerStore<T> => {
  const dispatch = (action: ReducerAction) => {
    setState(state => {
      return reducer(state, action)
    });
  };

  setState(defaultState);
  return { state: getState(), dispatch };
};

타입 부분에서 설명한 것과 같이 redux 함수는 getStatesetState를 인자로 받으면서 ReducerStore 타입을 반환하는 함수를 반환합니다.

redux 함수에 넘겨줘야 하는 값은 reducer 함수와 defaultState 두 개의 값이며, 이 반환값을 createStore에 넘겨주면 createStore에서 대응을 할 수 있도록 해야 합니다.

dispatch 함수의 경우, action을 전달받으며, reducer(state, action) 함수를 실행시켜 그 결괏값을 저장합니다. 여기에서 값이 변화하면 react에서 바로 반응하기 위해 setState를 통해 값을 수정하는 것을 확인할 수 있습니다.

createStore

현재 createStore 함수는 인자로 전달받은 값을 state에 저장합니다. redux 함수의 목적은 함수를 storestate에 저장하는 것이 아닌, dispatch를 이용해서 상태관리를 하는 것이므로 createStore의 값을 저장하는 부분에 수정이 필요합니다.

createStore 함수의 let state = defaultState; 부분이 아래와 같이 변경되어야 합니다.

  if (typeof createState === "function") {
    const {dispatch, state: reduceState} = (createState as ReducerReturnType<T>)(getState, setState);
    state = reduceState;

    return {getState, setState, onChange, dispatch};
  } else {
    state = createState;
    return { getState, setState, onChange };
  }

위 코드에서 createState 즉, 인자로 전달받은 값이 함수라면 그 함수에 getStatesetState를 인자로 전달해 dispatchstate 값을 반환받습니다. 여기서 반환받은 state 값을 storestate에 저장한 뒤, dispatch를 포함해서 값을 반환합니다.

만약, 함수를 전달받지 못했다면, 이전과 같이 인자로 전달받은 값을 storestate에 저장한 뒤, getState, setState, onChange 값을 반환합니다.

useGlobalStore

useReducer는 배열을 반환하는데, 첫 번째 값으로 state 값을, 두 번째 값으로 이를 수정할 수 있는 dispatch 함수를 반환합니다. 이와 같은 구조로 작업을 진행하려 합니다.

이는 매우 쉬운데, useGlobalStore의 반환값을 아래와 같이 수정하면 됩니다.

return [ selectedState(store.getState()), store.dispatch || store.setState ];

전달받은 store 값에 dispatch가 있다면 dispatch를 반환, dispatch가 없다면 setState 값을 반환합니다.

추후 업데이트 내용

localStorage를 이용한 persist 정도를 추가 기능으로 생각하고 있으며, 이 기능 작업이 완료된다면 패키지로 작업해 npm에 배포할 예정입니다.

728x90
728x90