Redux + Redux-thunk 동작원리

mechaniccoder·2021년 1월 19일
16
post-thumbnail

https://redux.js.org/assets/images/ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif

위의 이미지가 redux 동작원리의 전부입니다. (미들웨어가 빠졌지만 이는 뒤에서 설명하겠습니다.) 설명하기에 앞서 각각의 요소에 대해서 알아보죠.


Action

액션은 type 필드를 가진 자바스크립트 객체입니다. 쉽게 생각해서, 어떤 일이 일어났는지를 설명하는 이벤트라고 생각하셔도 무방합니다. 보통, typepayload 프로퍼티를 가지며 type은 어떤 액션인지를 나타내며, payload는 데이터를 담습니다.

액션은 dispatch를 통해 reducer 함수로 보내지며 기존의 state를 기반으로 새로운 state를 생성합니다.(불변성을 지키는 것이 redux의 원칙이죠.)


Dispatch

redux에서 dispatch는 액션을 reducer로 전달합니다. 즉, state를 업데이트하는 유일한 방법은 store.dispatch 함수를 호출하는 것입니다. 이벤트를 발생시키는 역할을 한다고 생각하시면 됩니다.


Reducer

reducer함수는 기존의 state와 action을 받아서 새로운 state를 만들어내는 함수입니다. 이벤트 리스너라고 생각하면 됩니다. 리듀서는 순수함수로서 몇 가지 규칙을 가집니다. 링크를 참고하세요.

root reducer

제가 좀 더 구체적으로 알고싶은 부분인데요. redux에서는 오직 하나의 리듀서만이 존재합니다. 실무에서는 유지 보수를 위해 여러 개의 리듀서를 만든 뒤에 하나의 루트 리듀서로 병합하는데요. 실제로 어떤 모습을 가지고 있는지 살펴보죠.

root reducer 살펴보기

아래와 같이 user, post 리듀서를 만듭니다.

function user(state = "minji", action) {
  return state;
}

function post(state = "post1", action) {
  return state;
}

리덕스의 combinedReducer api를 활용해서 리듀서들을 합칩니다.

const combined = combineReducer({ user, post });

이렇게 합쳐진 combined 이라는 루트 리듀서는 아래와 같은 모습을 가집니다.

function combined(state = {}, action) {
  return {
    user: user(state.user, action),
    post: post(state.post, action)
  };
}

즉, 우리가 dispatch를 통해 루트 리듀서를 호출하면 각각의 리듀서 함수가 실행되고 action이 존재하는 리듀서만이 새로운 상태를 반환하겠죠. 눈 여겨볼 점은 combined 함수의 첫번째 인자로 넘긴 state인데요. 현재 초기값이 {}이기 때문에 각각의 리듀서 함수의 첫번째 인자로 넘겨진 state.astate.bundefined입니다.

만약 초기 상태를 설정하고 싶다면 createStore api에서 2번째 인자인 preloadedState를 넘겨주면 됩니다.

사실 이러한 개념들을 이해하고 redux를 사용하는 것은 매우 쉽습니다. 그게 redux의 장점이죠. 하지만 좀 더 깊은 이해를 위해서 직접 구현해보도록 하겠습니다.


Redux 직접 구현하기

function createStore(reducer) {
  let state;
  const listeners = [];

  const getState = () => {
    return state;
  };

  const subscribe = (listener) => {
    listeners.push(listener);
    return function unsubscribe() { // 클로저가 활용되는 것을 확인할 수 있습니다.
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  };

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  };

  return {
    getState,
    subscribe,
    dispatch
  };
}

굉장히 간단하게 구현한 것이지만 위의 코드를 쳐보는 것만으로도 redux가 어떤식으로 동작하는지 알 수 있었습니다.


미들웨어

리덕스의 리듀서 함수에는 몇 가지 규칙이 있다고 앞서 설명드렸는데요. 이 규칙에 의해 리듀서는 side effect를 발생시켜선 안됩니다.

side effect는 간략하게 함수의 외부에 존재하는 state 혹은 behavior에 변화를 주는 것을 말합니다. 링크를 참고하세요.

따라서 리듀서에 비동기 로직이 존재할 수 없죠. 하지만, 우리는 api 서버에 네트워크 요청을 보내서 데이터를 가져와야만 합니다. 리덕스에서는 이를 어떻게 처리할까요?

미들웨어를 사용해서 비동기 로직을 처리합니다. 아래는 리덕스 미들웨어 동작원리를 그림으로 나타낸 것입니다.

https://redux.js.org/assets/images/ReduxAsyncDataFlowDiagram-d97ff38a0f4da0f327163170ccc13e80.gif

리덕스 미들웨어에는 대표적으로 두 가지 미들웨어가 있습니다. redux-thunk 그리고 redux-saga입니다. 오늘은 redux-thunk에 대해 알아보고 다음에 redux-saga를 정리해보겠습니다.

제가 이해한 redux-thunk는 결국 액션을 자바스크립트 객체뿐만 아니라 함수로 보내는 것입니다. 액션으로 함수를 보냈을때 이를 실행하는 것이죠. 직접 구현하면서 이를 이해해봅시다.

function createThunkMiddleware() {
  return ({dispatch, getState}) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    } 
    next(action);
  }
}

위의 함수에서 리턴되는 함수가 결국 thunk middleware함수입니다. 이 함수는 redux store에서 dispath, getState api를 받고, 다음 미들웨어로 액션을 보내는 next를 받습니다. 결국 마지막 함수로 우리가 dispatch할 액션을 인자로 받습니다.

우리가 함수를 dispatch하게 되면 action으로 받고 타입에 따라서 다르게 처리하는 것을 확인할 수 있습니다. 따라서 우리가 thunk middleware에 보낼 함수를 작성할때는 아래와 같이 작성합니다.

const getUser = (arg) => (dispatch, getState) => {
  // 비동기 로직
	getState(something);
    dispatch(something);
}

dispatch할 때는 아래와 같이 함수를 실행해서 보내주면 되겠죠. 그럼 dispatch와 getState를 인자로 받는 함수 자체를 thunk middleware로 넘기게 됩니다.

dispatch(getUser(args));

정리

  • 리덕스의 동작원리는 액션, 디스패치, 리듀서로 이루어진다.
  • 리듀서는 순수함수로서 side effect를 발생시키면 안된다.
  • 비동기 로직을 처리하기 위해 리덕스 미들웨어를 사용한다.
  • redux-thunk는 함수를 액션으로 보냈을때 처리하기 위함이다.

배가 고파서 마지막에 글을 잘 못 쓴 것 같지만 핵심이 되는 동작원리만 잘 파악하면 api는 공식문서에서 찾아서 사용하면 됩니다. 다음에는 redux-saga 그리고 공식문서에서 권장하는 @reduxjs/toolkit에 대해서 알아보겠습니다.

profile
세계 최고 수준을 향해 달려가는 개발자입니다.

0개의 댓글