IT/Study

Redux 내부 구조

프티 2022. 1. 29. 02:27
반응형

스토어

스토어는 내부적으로 리듀서와 1. 애플리케이션의 상태, 2. 이벤트 리스너, 3. dispatching 여부를 나타내는 값 (isDispatching: boolean)을 관리한다.

createStore API

  1. 내부 api 코드를 통해 구독하고 있는 리스너를 등록 및 관리하는 것을 알았다.
    1. action의 유효성 체크, dispatching 여부를 확인하고 등록된 리스너들을 forEach로 순회하며 수행하는 전반적인 과정을 알 수 있었다.

여기서 replaceReducer의 경우, 실제 구현 코드에서는 nextListeners와 currentListeners로 리스너 목록의 변경에 대한 상태를 분리해서 slice에 대한 비용을 줄였다고 한다.

하지만 리듀서 자체를 동적으로 활용한다는 점은 아직 와닿지가 않는 부분이다..

Redux의 스토어는 createStore 함수를 기반으로 동작한다.

따라서 고차 함수를 적용할 수 있으며, Redux에서는 enhancer라고 부른다고 한다.

*createStore API 내부 코드

/**
 * createStore API 내부
 *   (root)리듀서와 초기 상태를 인자로 받는다.
 **/// 모듈 패턴 - private 변수
(private) currentState: Object,
(private) currentReducer: Function - (state, action) => state,
(private) listeners: Array.<Function>
(private) isDispatching: Boolean

// API://   현재 상태를 반환
getState() {
  return currentState;
}

// API://  Change listener를 등록
subscribe(listener) {
  listeners.push(listener);

  return function unsubscribe() {
    var index = listeners.indexOf(listener);
    listeners.splice(index, 1);
  }
}

// API:
//  액션을 받아 리듀서로 처리한다. 다음 3가지의 동작으로 분리해볼 수 있다.
//    1. 전처리: Action의 유효성, 현재 Dispatching 여부
//    2. 리듀서: currentReducer에서 반환하는 다음 상태를 적용
//    3. 이벤트: 등록된 리스너들을 순서대로 수행
dispatch(action) {
  if (!isPlainObject(action)) throw Error
  if (typeof action.type === 'undefined') throw Error
  if (isDispatching) throw Error

// isDispatching은 리듀서에서 다시 dispatch를 호출하는 경우를 막기위해 사용한다.
  try {
    isDispatching = true;
    currentState = currentReducer(currentState, action);
  } finally {
    isDispatching = false;
  }

// `slice`는 리스너에서 subscribe와 unsubscribe API를 사용하는 경우,
// 현재 리스너 수행에 영향을 주지 않기 위함이다.
//
// 하지만 `slice` 비용은 비싸기 때문에 실제 구현은 조금 다르며, 동작 자체는 같다.
  listeners.slice().forEach(listener => listener());

  return action;
}

// API:
//   코드 분리(Code splitting), 핫-리로딩(hot reloading) 등의 기법이나,
//   리듀서 자체를 동적으로 사용할 때 필요할 수 있다.
replaceReducer(reducer) {
  currentReducer = reducer;

  dispatch({type: INIT});
}

// 상태 초기화 코드 수행
dispatch({type: INIT});

// API 반환
return {
  getState,
  subscribe,
  dispatch,
  replaceReducer
};

미들웨어

Redux의 미들웨어는 dispatching 과정에서 action이 리듀서에 도달하기 전 애플리케이션의 로직에 끼어들 수 있는 틈을 만들어준다.

이를 이해하기 위해서 compose라는 함수 중첩 과정을 이해해야 한다.

compose는 함수 중첩을 쉽게 표현하기 위해 사용되는 메소드이다.

compose(f, g, h) = (...args) => f(g(h(...args)));

미들웨어는 action이 리듀서에 도달하기 전에 먼저 수신하여 action에 대한

  • 검증
  • 모니터링
  • 외부 API와의 연동
  • 비동기 처리

등을 추가적으로 수행할 수 있도록 한다.

만약 미들웨어가 없다면 action 생성자에서 이러한 작업을 처리하거나 dispatch를 몽키패치(런타임 중 코드를 수정하는 것)하여 처리해야 하는데 이러한 경우,

중복, 복잡도, 비 순수함수 등의 유지 보수가 어려운 문제가 발생한다.

미들웨어는

  1. createStore API인 { getState, dispatch }를 인자로 받는다.
  2. next라는 체이닝 함수를 전달받는 새로운 wrapDispatch 함수를 반환한다.

위 두 과정에 의해 미들웨어는 3개의 중첩된 함수를 구현한다.

function middleware({getState, dispatch}}) { //1.
  return function wrapDispatch(next) { //2.
    return function dispatchToSomething(action) {
      // do something...
      return next(action);
    }
  }
}

next 함수는 백엔드 미들웨어에서 활용한 것처럼 다음 함수에 진입하기 위해 필요한 함수이다.

주의해야할 점은 중간에 createStore API의 dispatch를 호출할 경우 action이 다시 처음으로 돌아간다는 점이다.

이제 미들웨어와는 다른 applyMiddleware API를 살펴보자.

function applyMiddleware(...middlewares) {
  // applyMiddleware는 기존 createStore의 고차 함수를 반환한다.
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []

    // 미들웨어들에게 인자로 전달되는 객체이다.
    // dispatch가 단순히 store.dispatch의 참조를 전단하는 것이 아니라,
    // 함수를 한번 더 감싸 사용하는 점을 기억하자.
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }

    // 미들웨어들이 반환하는 체인 함수들(= wrapDispatch)을 가져온다.
    chain = middlewares.map(middleware => middleware(middlewareAPI))

    // 미들웨어가 반환하는 체인 함수들을 중첩시킨 후 새로운 dispatch 함수를 만든다.
    dispatch = compose(...chain)(store.dispatch)

    // applyMiddleware를 통해 반환된 createStore 고차 함수는
    //  기존 스토어와 동일한 API, 그리고 새로 만들어진 dispatch 함수를 반환한다.
    return {
      ...store,
      dispatch
    }
  }
}

여기서 middlewareAPI 부분을 보면,

store.dispatch 함수를 한 번 더 감싸서 체인 함수들로 전달하는 것을 볼 수 있다.

이렇게 되면 체인 함수들인 미들웨어들은 항상 store.dispatch를 인자로 받게 된다.

왜냐하면 compose(...chain)(store.dispatch)에 의해 action으로 함수가 넘어가면,

const asyncFunctionMiddleware = storeAPI => next => action => {
  // If the "action" is actually a function instead...
  if (typeof action === 'function') {
    // then call the function and pass `dispatch` and `getState` as arguments
    return action(storeAPI.dispatch, storeAPI.getState)
  }

  // Otherwise, it's a normal action - send it onwards
  return next(action)
}

다음과 같은 비동기 미들웨어에서 action을 함수로 인식하고 action을 실행한다.

이렇게 넘어온 action 함수에 dispatch, getState 인자가 넘어오면 비동기 처리를 하고,

그 결과로 받은 데이터를 비로소 dispatch의 payload에 담아 action과 함께 reducer로 보내게 되는 것이다.

그런데 코드만 놓고 본다면 만약 비동기 처리를 하지 않는 경우에도 action이 함수로 들어가는 경우도 생기는 것이 아닌가 궁금하지만 아직 해결하지 못하였다..

const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)

// Write a function that has `dispatch` and `getState` as arguments
const fetchSomeData = (dispatch, getState) => {
  // Make an async HTTP request
  client.get('todos').then(todos => {
    // Dispatch an action with the todos we received
    dispatch({ type: 'todos/todosLoaded', payload: todos })
    // Check the updated store state after dispatching
    const allTodos = getState().todos
    console.log('Number of todos after loading: ', allTodos.length)
  })
}

// Pass the _function_ we wrote to `dispatch`
store.dispatch(fetchSomeData)
// logs: 'Number of todos after loading: ###'

리듀서

Redux는 Reducer + Flux의 줄임말이다.

그만큼 리듀서는 리덕스에서 핵심적인 개념이라고 할 수 있다.

리듀서는 state와 action을 받아 새로운 상태를 반환한다.

이는 곧 리듀서가 순수함수로 정의되었음을 의미한다.

reducer: (previousState, action) => newState

리듀서는 기본적으로 단위 상태에 대한 단위 리듀서들의 조합을 구성할 수 있다.

어느 애플리케이션이 되었든 수십에서 수백가지의 상태들을 관리하는데,

단일 리듀서로는 그 많은 상태들을 관리하리란 쉽지 않기 때문이다.

따라서 작은 단위의 리듀서들을 정의하고 마지막에 combineReducer API를 통해 각 리듀서들을 조합하여 하나의 큰 루트 리듀서로 만들 수 있다.

function combineReducers(reducers) {
  return (state = {}, action) => {
    return Object.keys(reducers).reduce((result, key) => {
      const reducer = reducers[key]
      const prevState = state[key]

      result[key] = reducer(prevState, action)
      return result
    }, {})
  }
}

reducers = {
  a: aReducer,
  b: bReducer,
  c: cReducer
};
const combinedReducer = combinReducers(reducers);

/*

조합된 최종 리듀서(combinedReducer)는 다음과 같다.

function(state, action) {
  return {
    a: aReducer(state.a, action),
    b: bReducer(state.b, action),
    c: cReducer(state.c, action)
  }
}

combinedReducer를 호출하면 aReducer, bReducer, cReducer가 모두 호출되고 새로운 객체를 반환한다.

*/

각 리듀서가 Array.prototype.reduce API에 전달되는 형태이기 때문에 리듀서라고 부른다.

참고한 위 코드처럼 실제로 reduce를 사용하지는 않지만 개념적으로 분산된 리듀서들을 하나의 리듀서로 감소시킨다는 의미와는 부합한다.

코드에서 볼 수 있듯,

combineReducer API는 1-depth의 상태를 가지고 처리한다.

하지만 이를 재귀적으로 적용하면 상태가 n-depth여도 이를 하나의 전체 상태로 관리할 수 있다.

액션에 따른 상태 변화 유무

리듀서는 항상 새로운 객체를 반환하지만,

만약 반환한 상태가 변하지 않았다면 이를 어떻게 알 수 있을까?

변화가 없을 경우 리듀서는 기존의 상태를 반환해야 한다.

let hasChanged = false
const nextState = {}

for (let i = 0; i < finalReducerKeys.length; i++) {
  const key = finalReducerKeys[i]
  const reducer = finalReducers[key]
  const previousStateForKey = state[key]
  const nextStateForKey = reducer(previousStateForKey, action)

  nextState[key] = nextStateForKey
  hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state

위 코드와 같이 hasChange를 nextStateForKey !== previousStateForKey에 따라 boolean 값을 할당해주어 hasChange가 변했다면 nextState를, 변하지 않았다면 기존의 state를 반환한다.

이렇게 리덕스의 내부 구조를 살펴보았다.

리덕스의 장점 및 활용

내부 구조를 살펴보면서 알 수 있듯 리덕스에는 3가지 원칙이 존재한다.

  • 신뢰할 수 있는 한 가지 소스
  • 상태는 무조건 읽기 전용이다.
  • 변화는 순수 함수로 이루어진다.
리덕스는 데이터가 집중화(Centralized) 되어 있어서 예측 가능하며(Predictable), 데이터 흐름이 단방향이라서 
디버깅하기 쉽다(Debuggable). 또 리덕스와 연관된 좋은 생태계가 구축되어 있어서 필요에 맞게 유연하게(Flexible) 구현할 수 있다.

 

하지만 프로젝트의 규모가 크지 않은데도 리덕스를 사용한다면 불필요한 라이브러리만 하나 더 import하여 애플리케이션 번들 사이즈만 커질 뿐이다.

따라서 리액트로 단방향 데이터 흐름을 구현할 경우 과도한 prop drilling이 생기거나 디버깅이 어려운 경우 사용할 수 있도록 해야 한다.

출처

https://meetup.toast.com/posts/111

https://ko.redux.js.org/tutorials/fundamentals/part-6-async-logic

https://velog.io/@wooder2050/리덕스Redux는-왜-쓰는-건데

반응형

'IT > Study' 카테고리의 다른 글

Cross Browsing  (0) 2022.05.24
Renderer Process  (0) 2022.05.21
몰랐던 내용 정리 (상시 업데이트)  (0) 2021.11.26
동적 타입 언어  (0) 2021.11.23
객체 지향 프로그래밍 기본 용어  (0) 2021.11.23