[React] Redux Middleware 직접 구현해보기

2020. 9. 19. 19:00Frontend

 

 

 

https://medium.com/@bibhutipd/custom-middleware-in-django-71a72f3f8f46

 

 

 

Overview

 

미들웨어 (middleware)란 소프트웨어 공학에서 사용되는 오래된 개념으로, 운영 체제와 운영 체제 사이에서 실행되는 프로그램 사이에 존재하는 소프트웨어를 의미합니다. 말 그대로 운영 체제와 프로그램 '중간에' 끼어서 부가적인 일들을 처리하는 소프트웨어인 셈입니다.

 

 

미들웨어는 어떤 특정 소프트웨어를 지칭한다기 보다는 말 그대로 '추상적인 개념'이기 때문에 이 개념을 웹 서비스에서도 받아들여서 웹 서비스를 개발할 때 View와 Logic 을 분리하여 사이드 이펙트(Side effect)를 처리하거나, 중간중간 로그(Log)를 찍거나(Loggin)하는 등의 부가적인 액션들을 이 미들웨어를 통해서 처리합니다. 리액트 서비스를 예로 들자면 대표적으로 사가(Saga)가 있을 것이고, Node.js 기반의 Express 서버를 예로 들자면, Cors, bodyParser, CookieParser등이 있을 것입니다.

 

 

이번 포스팅에서는 지난 포스팅에서 만들었던 간단한 리덕스 스토어 를 바탕으로 스토어에 Action이 추가될 시에 간단하게 해당 액션을 로깅하는 미들웨어를 추가해보면서 미들웨어의 개념과, 동작 개념에 대해서 알아보도록 하겠습니다.

 

 

Simple Store

 

지난 포스팅에서 다루었던 간단한 스토어 입니다. 리덕스에서 권장하는 방법은 "스토어의 레퍼런스에 직접 접근해서 값을 변경하지 말고, reducer를 이용해라" 이기 때문에 createStore에서는 현재 상태를 받는 getState와, state를 업데이트하는 dispatch함수, 그리고 해당 상태를 감지하는 subscribe함수를 리턴합니다.

 

아래 ActionCreator함수는 필수적인 함수는 아니지만, 모든 액션마다 일일이 객체를 생성하는 것은 귀찮기도 하고, 실수할 확률이 높기 때문에 타입과 payload를 입력하면 dispatch할 수 있는 액션 객체를 생성해주는 간단한 함수입니다.

 

이전과 조금 달라진 점은 createStore함수가 middlewares라는 배열을 추가적인 파라미터로 받고 있다는 점입니다. 이 middleware들은 이후 dispatch 함수에 Wrapping 되어서, dispatch를 호출해서 상태를 업데이트하려고 할때 중간에 사용자가 정의한 임의의 액션들을 실행합니다. (이번 예제에서는 Logging을 수행할 것입니다.)

 

 

redux-middleware.js

export function createStore(reducer, middlewares = []) {
  
  let state;
  const listeners = [];
  const publish = () => {
    listeners.forEach(({ subscriber }) => {
      subscriber.call();
    });
  };
  const dispatch = (action) => {
    state = reducer(state, action);
    publish();
  };
  const subscribe = (subscriber) => {
    listeners.push(subscriber);
  };

  const getState = { ...state };
  const store = {
    dispatch,
    getState,
    subscribe
  };

  return store;
}

export const actionCreator = (type, payload = {}) => ({
  type,
  payload: { ...payload }
});

 

 

미들웨어 추가와 Curring

 

미들웨어를 본격적으로 추가하기에 앞서서 Curring이라는 개념을 잠시 짚고 넘어갈 필요가 있습니다.  Curring이란 인자를 여러 개 받는 함수를 분리하여 인자를 하나씩만 받는 함수의 체인(Chain)으로 만드는 기법을 의미합니다. 함수를 재사용하는데 유용하게 쓰일 수 있는 기법으로 자바스크립트에서는 다음과 같이 구현이 가능합니다. 

 

function add(a1, a2) {
  return a1 + a2;
}

function add2(a1) {
  return function (a2) {
    return a1 + a2;
  };
}

console.log(add(1, 2));
console.log(add2(1)(2));

 

 

add, add2함수는 둘다 두 인자를 더하는 간단한 로직으로 구성된 함수이지만 add함수가 두 인자를 한번에 받아서 두 인자를 더한 값을 바로 리턴하는데 반해,  add2함수는 더할 첫 번째 인자를 집어넣고 호출하면 해당 인자를 해당 함수의 로컬 스코프로 가지고 있고, 두번째 인자를 받는 함수를 리턴합니다. 

 

 

왜 커링을 통해 미들웨어를 구현하나?

 

간단한 함수인 경우에는 커링이 불편하게 느껴지거나 굳이(?) 써야하나 라는 생각이 들 수도 있지만, 커링을 통해 리턴된 함수는 자바스크릷트의 실행 컨텍스트에 의해 부모 함수의 스코프를 들고 있기 때문에 여러 군데에서 재사용하기 편리하다는 장점이 있습니다. 지금부터 설명할 리덕스 미들웨어도 이러한 커링 개념을 통해 구현한 것입니다. 

 

 

미들웨어를 커링을 통해 구현한 이유는 원본 dispatch를 몽키패칭(런타임에 로직을 추가하거나, 변경하는 등의 수정작업을 진행하는 것)하지 않고, 여러 종류의 미들웨어를 단지 기존 dispatch에 wrapping하여 사용하기 위함입니다.  dispatch가 일어났을 때, 로깅을 해주고 싶은데, 이를 dispatch를 호출할때마다 로깅을 호출하게 된다면 수많은 dispatch에 해당 코드를 추가해야 하므로 여간 불편한 일이 아닙니다. 그렇다고 store.dispatch를 직접 변경해서 로깅하는 코드를 추가하게 된다면, 로깅하는 기능 이외에 다른 기능을 추가하고 싶을때는 상당히 곤란한 상황에 처하게 됩니다. 

 

 

따라서 권장되는 방법은 기존 dispatch를 변경하지 말고, 기존 dispatch에 미들웨어를 덮어씌워서 미들웨어의 기능을 수행한 이후에 기존 dispatch를 호출하는 방식입니다. 즉 dispatch액션에 로깅을 추가하고 싶다면 dispatch메소드가 호출되었을 때, 이 메소드가 dispatch를 감싸고 있는 logger함수를 먼저 호출해서 logging작업을 수행한 이후에, 그 안에서 다시 dispatch를 호출해 주는 식으로 구현하는 것입니다. 따라서 구현할 미들웨어는 기본적으로 다음과 같은 형태를 띄게 됩니다.

 

 

const middleware1 = (store) => (dispatch) => (action) => {
	// 부가적인 액션들을 여기 위아래로!
	dispatch(action);
}

function middleware2(store) {
	return function(dispatch) {
          return function(action) {
          	// 부가적인 액션들을 여기 위아래로!
              dispatch(action)
          }
    }
}

// 두 함수는 같은 기능을 하는 (커링을 통해 구현된 미들웨어)

 

 

만약 로깅을 하는 logger 미들웨어를 추가하고 싶다면 다음과 같이 구현할 수 있겠죠. 아래는 dispatch호출시 중간에서 액션의 타입과 현재 상태를 로깅하는 미들웨어의 예시입니다. 

const logger = (store) => (dispatch) => (action) => {
	console.log(action.type, store.getState());
	dispatch(action);
}

 

 

스토어에서는?

 

스토어에서는 해당 미들웨어를 다음과 같이 커링을 이용해서 감싸줍니다. 여기서 Array.reverse()를 호출해서 미들웨어 배열의 순서를 한번 뒤집어주는 이유는 미들웨어 배열의 처음 인자가 가장 먼저 실행될 수 있도록, 즉 가장 바깥을 감싸고 있는 미들웨어가 되도록 처리하기 위함입니다.

 

 

Middleware 추가

// Middlewares
  middlewares = Array.from(middlewares).reverse();
  let wrapDispatch = store.dispatch;

  middlewares.forEach(middleware => {
    wrapDispatch = middleware(store)(wrapDispatch);
  })

  return { ...store, dispatch: wrapDispatch};

 

 

최종 코드

export function createStore(reducer, middlewares = []) {
  let state;
  const listeners = [];
  const publish = () => {
    listeners.forEach(({ subscriber }) => {
      subscriber.call();
    });
  };
  const dispatch = (action) => {
    state = reducer(state, action);
    publish();
  };
  const subscribe = (subscriber) => {
    listeners.push(subscriber);
  };

  const getState = { ...state };
  const store = {
    dispatch,
    getState,
    subscribe
  };

  // Middlewares
  middlewares = Array.from(middlewares).reverse();
  let wrapDispatch = store.dispatch;

  middlewares.forEach((middleware) => {
    wrapDispatch = middleware(store)(wrapDispatch);
  });

  return { ...store, dispatch: wrapDispatch };
}

export const actionCreator = (type, payload = {}) => ({
  type,
  payload: { ...payload }
});

 

 

Conclusion

 

웹 서비스에서의 미들웨어는 기존 메소드를 손상시키지 않고 부가적인 액션들을 처리할 수 있는 방법을 제공합니다. 다음 포스팅에서 살펴볼 리액스 side-effect 라이브러리인 Saga도 미들웨어 중 하나인데, action dispatch가 일어났을 때, 중간에 API call등의 side-effect를 처리하고 기존 dispatch를 호출하는 방법을 제공하기 때문입니다. 

반응형