본문으로 바로가기

리덕스-미들웨어 만들기

category React 2021. 2. 16. 02:41

노션에 정리한 내용을 복사 붙여넣기 했기 때문에 내용이 깨질 수 있습니다 

https://github.com/CodePotStudio/React-Challenge/blob/main/2_2_week/jeho_middleware.md

 

CodePotStudio/React-Challenge

[Code Pot,] React 챌린지 레포지터리. Contribute to CodePotStudio/React-Challenge development by creating an account on GitHub.

github.com


미들웨어의 커링, 중첩 함수 구조가 잘 이해되지 않아서 미들웨어가 만들어지는 과정을 알아보려고 한다

import React, {useCallback} from 'react';
import Counter from "../components/Counter";
import {useSelector, useStore} from "react-redux";
import {decrease, increase} from "../modules/counter";

const CounterContainer = () => {
    const number = useSelector(state=>state.counter);
    const store = useStore();

    return (
        <div>
            <Counter number={number}
                     increase={()=>store.dispatch(increase())}
                     decrease={()=>store.dispatch(decrease())}/>
        </div>
    );
};

export default CounterContainer;

store 내부에 있는 dispatch를 직접 건드려야하기 때문에 useStore를 이용해서 Counter App을 구현했다

디스패치가 발생할 때 마다 액션값, 이전,이후 값을 로깅하는 미들웨어를 구현해보려고 한다

1. 직접 로깅하기


말 그대로 디스패치가 일어나기 전과 후에 하드코딩해서 로그를 출력시키는 방법이다

const CounterContainer = () => {
    const number = useSelector(state=>state.counter);
    const store = useStore();

    return (
        <div>
            <Counter number={number}
                     increase={()=>{
                         const action = increase();
                         console.log(store.getState());
                         console.log(action)
                         store.dispatch(action);
                         console.log(store.getState());
                     }}
                     decrease={()=>{
                         const action = decrease();
                         console.log(store.getState());
                         console.log(action)
                         store.dispatch(action);
                         console.log(store.getState());
                     }}/>
        </div>
    );
};

export default CounterContainer;

결과


 

원하던 결과를 얻긴 했지만 매번 이렇게 하드코딩을 할 순 없을 것 같다

2. 디스패치를 감싸기


말 그대로 디스패치를 다른 함수에 넣어두고 그 함수 내부에 로그를 출력하게 코드를 추가해주는 방법이다

이번엔 로그들을 액션 이름으로 그룹화 해서 좀 더 보기 좋게 만들어봤다

const CounterContainer = () => {
    const number = useSelector(state=>state.counter);
    const store = useStore();

    function dispatchAndLog(store,action){
        console.group(action);
        console.log('prev State',store.getState());
        store.dispatch(action);
        console.log('next State',store.getState());
        console.groupEnd();
    }

    return (
        <div>   
            <Counter number={number}
                     increase={()=>dispatchAndLog(store,increase())}
                     decrease={()=>dispatchAndLog(store,decrease())}/>
        </div>
    );
};

export default CounterContainer;

이제 dispatchingAndLog 함수를 dispatch처럼 사용할 수 있다

결과


 

가독성도 좋아졌고 하드코딩 또한 줄었지만 특별한 함수를 생성해야하는 것과 대체된 함수를 불러오는 것은 별로 좋지 못한 것 같다

3. 디스패치 몽키패칭하기


  • 몽키패치

dispatch 메서드를 수정하는 일이라면 별로 어렵지 않을 것 같다

원래 메서드에 위에서 만들어 놓은 dispatchAndLog 함수로 바꿔치기하면 끝인 일이니까

    const store = useStore();

    store.dispatch = function dispatchAndLog(action){
        console.group(action);
        console.log('prev State',store.getState());
        store.dispatch(action);
        console.log('next State',store.getState());
        console.groupEnd();
    }

이렇게 작성하고 코드를 돌렸는데 너무 멍청했다

dispatch가 호출되면 계속해서 dispatch를 호출하는 무한 재귀에 빠지는 게 된 것

const next = store.dispatch;

store.dispatch = function dispatchAndLog(action){
    console.group(action);
    console.log('prev State',store.getState());
    next(action);
    console.log('next State',store.getState());
    console.groupEnd();
}

그래서 dispatchAndLog 외부에 기존 dispatch를 next에 저장해둔 뒤 dispatchAndLog에 저장해둔 next를 통해 dispatch를 실행시키도록 했다

실패


잘 실행되는 줄 알았더니 dispatch가 실행할 때 마다 로그 안에 로그가 중복되어서 출력되고 있었는데

const CounterContainer = () => {
    const number = useSelector(state=>state.counter);
    const store = useStore();
    const next = store.dispatch;

    store.dispatch = function dispatchAndLog(action){
        console.group(action);
        console.log('prev State',store.getState());
        next(action);
        console.log('next State',store.getState());
        console.groupEnd();
    }

    return (
        <div>
            <Counter number={number}
                     increase={()=>store.dispatch(increase())}
                     decrease={()=>store.dispatch(decrease())}/>
        </div>
    );
};

내가 짠 코드의 흐름을 보면 다음과 같다

  1. next 에 store.dispatch(원본)가 저장된다
  2. store.dispatch는 dispatchAndLog로 대체된다 (원본 소멸)
  3. dispatch가 발생해서 리렌더링된다
  4. next에 store.dispatch(원본이 아닌 dispatchAndLog)가 다시 저장된다

해결


이걸 해결하려고 많은 삽질을 했다 (useEffect를 써서 첫 렌더링에만 next에 원본 담기, 리덕스 라이브러리들어가보기 등등)

애초에 처음부터 store를 몽키패칭하는 위치가 잘못되있었다는 걸 겨우 깨달았다

store는 최상위 컴포넌트에서 createStore로 만든 뒤 useStore를 통해서 불러오는 각 컴포넌트에서 사용할 수 있게끔 하는 것이니까 최상위에서 store를 만드는 시점에 메서드를 수정하면 되는 것이었다

// in index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore} from "redux";
import rootReducer from "./modules";
import {Provider} from "react-redux";

const store = createStore(rootReducer);
const next = store.dispatch;

store.dispatch = function dispatchAndLog(action){
    console.group(action);
    console.log('prev State',store.getState());
    next(action);
    console.log('next State',store.getState());
    console.groupEnd();
}

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
  document.getElementById('root')
);

이렇게 컴포넌트 바깥에서 함수를 수정해주면 렌더링에 영향을 받지 않고 원하는 대로 작동한다

 

문제점


여전히 문제점은 남아있는데

const store = createStore(rootReducer);
const next = store.dispatch;

function patchStoreToAddLogger(store){

    store.dispatch = function dispatchAndLog(action){
        console.group(action);
        console.log('prev State',store.getState());
        next(action);
        console.log('next State',store.getState());
        console.groupEnd();
    }
}
// 디스패치가 발생하면 그냥 로그를 찍는 더미 함수
function patchStoreToAddCall(store){

    store.dispatch = function dispatchAndCall(action){
        console.group(action)
        console.log('변화합니다');
        next(action);
        console.groupEnd();
    }
}

이 처럼 두 개 이상의 미들웨어를 적용하고 싶으면 어떻게 해야할까?

함수로 만든 둘을 따로 분리해서 patchStoreToAddCall(store); , patchStoreToAddLogger(store);

와 같이 사용한다면 하나씩 사용할 수는 있겠지만

store의 dispatch는 한 개다 이걸 로그용으로 대체한 상태로 다른 기능을 동시에 사용하기는 힘들어 보인다

4. 몽키패칭 숨기기


여태까진 dispatch를 바꿔치기 했지만 새로운 disaptch를 반환한다면 어떨까?

function logger(store){
    const next = store.dispatch;

    return function dispatchAndLog(action){
        console.group(action);
        console.log('prev State',store.getState());
        const result = next(action);
        console.log('next State',store.getState());
        console.groupEnd();
        return result;
    }
}
function call(store){
    const next = store.dispatch;
    store.dispatch = function dispatchAndCall(action){
        console.group(action)
        console.log('변화합니다');
        const result = next(action);
        console.groupEnd();
        return result;
    }
}

구현하다 알게된 사실인데 store 내부에 구현되어있는 dispatch는 리턴값이 액션으로 되어있다 그래서 내가 구현한 call과 log도 액션값을 리턴하도록 따라해주었다

다수의 미들웨어를 몽키패칭하게 도와주는 함수를 작성해보자

function applyMiddlewareByMonkeypatching(store, middlewares) {
    middlewares = middlewares.slice();
    middlewares.reverse();

    middlewares.forEach(middleware => (store.dispatch = middleware(store));
}
// 적용시
applyMiddlewareByMonkeypatching(store,[logger,call]);

store와 작성한 미들웨어들을 파라미터로 받고 실제 실행 때는 자기순서를 지킬 수 있도록 역순으로 바꾼 다음

store.dispatch에 미들웨어 함수의 리턴값를 넣어준다

middlewares.forEach(middleware => (store.dispatch = middleware(store));

이게 무슨 의미지? 싶었다 결국 마지막 함수의 값으로 store.dispatch가 덮어씌워지는 거 아닌가?

applyMiddlewareByMonkeypatching


이 함수가 실행되는 중의 middlewares의 배열 상태는 [call,logger]이고 forEach가 돌아가면서 store에 실제로 어떻게 저장되는지 확인해보자

이 흐름을 살펴보는 중에 왜 미들웨어 함수 내부에 next를 저장해야하는지도 깨닫게 되었다

  1. 첫 번째로 call(store)의 리턴 값이 store.dispatch에 저장되야하는데 그 내부를 살펴보면 다음과 같다
  2. store.dispatch = function call(store){ // 여기서 call함수의 next에 원본 store.dispatch가 저장된다 const next = store.dispatch; // 이 return에 의해 dispatchAndCall가 store.dispatch에 저장된다 return function dispatchAndCall(action){ console.group(action) console.log('변화합니다'); // 여기의 next는 원본 store.dispatch다 const result = next(action); console.groupEnd(); return result; }; }();
  3. 두 번째로 logger(store)의 리턴값도 store.dispatch에 저장되며 내부 또한 비슷하다
  4. //현재 dispatch에는 call에 의해 dispatchAndCall가 저장되어있다 store.dispatch = function logger(store){ // 그러므로 logger가 가진 next에는 dispatchAndCall가 저장된다 const next = store.dispatch; //이 return에 의해 dispatchAndLog가 store.dispatch에 저장된다 return function dispatchAndLog(action){ console.group(action); console.log('prev State',store.getState()); // 여기의 next는 dispatchAndCall이다 const result = next(action); console.log('next State',store.getState()); console.groupEnd(); return result; }; }();

결과


미들웨어 체이닝에 의해서 할당해준 dispatch들이 모두 실행되는 것을 볼 수 있다

5. 몽키패칭 제거하기


거의 다 온 것 같다

체이닝을 조금 바꿀껀데 미들웨어 안에 변수로 현재의 dispatch를 저장하는 것이 아닌 외부에서 store.dispatch를 넘겨 줄 것이다

function logger(store){
        // const next = store.dispatch 대신
    return function wrapDispatchAndLog(next){
        return function dispatchAndLog(action){
            console.group(action);
            console.log('prev State',store.getState());
            const result = next(action);
            console.log('next State',store.getState());
            console.groupEnd();
            return result;
        };
    }
}
function call(store){
        // 마찬가지
    return function wrapDispatchAndCall(next){
        return function dispatchAndCall(action){
            console.group(action)
            console.log('변화합니다');
            const result = next(action);
            console.groupEnd();
            return result;
        };
    }
}

이제 몽키 패치가 아니게 되었으니 applyMiddlewareByMonkeypatching이름도 applyMiddleware로 바꾸어 주자

function applyMiddleware(store, middlewares) {
    middlewares = middlewares.slice();
    middlewares.reverse();

    middlewares.forEach(middleware=>store.dispatch = middleware(store)(store.dispatch));
}

내부 변수인 const next = store.dispatch로 저장해서 체이닝을 유지하는 것이 아니라

함수로 한 번 더 감싼뒤 매개변수로 store.dispatch자체를 넘겨줌으로써 체이닝을 유지한체 next로 사용할 수 있게되었다

arrow function으로 바꿔본 미들웨어들

const logger = store=>next=>action=>{
    console.group(action);
    console.log('prev:',store.getState());
    const result = next(action);
    console.log('next:',store.getState());
    console.groupEnd();
    return result;
}
const call = store=>next=>action=>{
    console.group(action);
    console.log('변화합니다');
    const result = next(action);
    console.groupEnd();
    return result;
}

이제 드디어 미들웨어의 구조를 이해할 수 있게 되었다..

'React' 카테고리의 다른 글

Styled-components  (0) 2021.02.26
Redux-saga  (0) 2021.02.25
Redux 파일 구조  (0) 2021.02.15
프레젠테이셔널, 컨테이너 컴포넌트 패턴  (0) 2021.02.13
불변성  (0) 2021.02.02