리액트 학습 노트 (중급) 4일차 정리

아놀드·2021년 8월 8일
1
post-thumbnail

미들웨어란?

액션과 리듀서 중간에서 추가적인 작업이나 전처리등을 수행하는 부분.
(proxy 비슷한 느낌)

액션 -> 리듀서

액션 -> 미들웨어 -> 리듀서

미들웨어에서 할 수 있는 일은

  1. 특정 조건에 따라 액션을 무시
  2. 액션과 reducer를 통해 바뀐 state를 로깅
  3. 액션을 다르게 가공해서 리듀서에게 전달
  4. 다른 액션을 발생
  5. 비동기 api의 결과값을 리듀서에게 전달

등등이 있다.

더 많은 정보
출처 https://react.vlpt.us/redux-middleware/

미들웨어를 직접 구현하는 법까지 가르쳐준다.

applyMiddleware

추가하고 싶은 미들웨어들을 징검다리처럼 이어주는 함수.

미들웨어가 도대체 어떤 원리로 동작하는지 찾아봤다.

핵심은 이 applyMiddleware 함수인데 정말 감각적인 구현이다.

함수 합성을 재귀적인 사고 방식으로 구현했다.

어떻게 동작하는지 차근차근 알아보자.

// 미들웨어들을 이어주는 함수
const applyMiddleware = (...middlewares) => {
    // 인자로 넘어온 미들웨어 함수들을 새로 복사한 다음 거꾸로 뒤집기
    middlewares = [...middlewares].reverse();
    
    // 미들웨어들을 연결
    const dispatch = middlewares.reduce((dispatch, middleware) => (
        dispatch = middleware(store)(dispatch)
    ), store.dispatch);
    
    // 스토어의 복사본 리턴
    return Object.assign({}, store, { dispatch });
}

// 로깅 미들웨어
const logger = store => next => action => {
    console.log('dispatching', action);
    const result = next(action);
    console.log('next state', store.getState());

    return result;
};

// 충돌 리포팅 미들웨어
const crashReporter = store => next => action => {
    try {
        return next(action);
    }
    catch (err) {
        console.error('Caught an exception!', err);
        throw err;
    }
};

// 미들웨어들을 연결
applyMiddleware(logger, crashReporter);

코드를 차근차근 해석해보자.

    // 인자로 넘어온 미들웨어 함수들을 새로 복사한 다음 거꾸로 뒤집기
    middlewares = [...middlewares].reverse();
  1. 인자로 받은 미들웨어들을 새로 복사한 다음 거꾸로 뒤집어줬다.
    그 이유는 마지막으로 받은 미들웨어는 next를 호출할 때
    store.dispatch가 호출되어 reducer에 action이 전달돼야 하기 때문이다.
    (근데 새배열에 복사하는 이유는 모르겠다. 어차피 배열 안에 있는 미들웨어 함수들을 호출해서 상태를 바꾸는데 무슨 의미인지 아시는 분 있으면 알려주세요.)
    // 미들웨어들을 연결
    const dispatch = middlewares.reduce((dispatch, middleware) => (
        dispatch = middleware(store)(dispatch)
    ), store.dispatch);
  1. 미들웨어 함수들을 순회하며 인수를 차례대로 store, dispatch를 넣어줬다.
    그러면 새로운 dispatch 함수가 리턴되는데
    이 새로운 dispatch 함수를 다음 미들웨어에게 전달
    하는 식으로
    미들웨어 체이닝이 완성된다.

예제에 나온 코드로 알아보자.

2 - 1.
이 예제에서는 첫 번째 미들웨어로 crashReporter함수가 순회된다. (reverse 했기 때문)
crashReporter(store)(dispatch)를 하게 되면

action => {
    try {
        return next(action);
    }
    catch (err) {
        console.error('Caught an exception!', err);
        throw err;
    }
};

내부적으로 action 객체를 받아서 reducer에게 dispatch를 하고
그 과정에서 에러가 났을 땐 에러를 보고하는 함수가 리턴된다.

2 - 2.
리턴된 dispatch를 다음 미들웨어인 logger에게 넘겨주면

action => {
  console.log('dispatching', action);
  const result = next(action); // crashReporter에게 전달
  console.log('next state', store.getState()); // 상위의 상위 스코프에 있는 store 인스턴스
  
  return result;
};

action을 로깅하고 crashReporter 미들웨어에게 전달한 다음
crashReporter 미들웨어가 본인의 일처리를 끝나고 reducer에게
action 전달까지 완료돼서 상태가 바뀌면
다음 상태를 로깅하는 함수가 리턴된다.

2 - 3.

    // 스토어의 복사본 리턴
    return Object.assign({}, store, { dispatch });

리턴된 dispatch 함수를 스토어의 새로운 dispatch로 덮어씌운다.
그러면 action을 dispatch했을 때
action -> logger -> crashReporter -> reducer의 흐름으로 이어지게 된다.

알면 정말 별 거 없다.
그저 직렬적으로 연결된 미들웨어들이
중간에서 자기 할 일을 한 뒤에 다음 턴으로 넘기는 방식이다.
마치 함수형 프로그래밍에서 파이프라인을 이어주는 느낌과 비슷하다.
함수형의 파이프라인은 나열된 함수들을 순서대로 실행시켜주는 함수라면
applyMiddleware는 나열된 함수들이 내부적으로 다음 함수를 실행시키도록 합성하는 함수다.

조금 더 직관적인 구현

// 미들웨어들을 이어주는 함수
const applyMiddleware = (...middlewares) => {
    // 미들웨어들을 연결
    const dispatch = middlewares.reduceRight((dispatch, middleware) => (
        dispatch = middleware(store)(dispatch)
    ), store.dispatch);
    
    // 스토어의 복사본 리턴
    return Object.assign({}, store, { dispatch });
}

사람마다 다르겠지만 개인적으로 이 버전이 좀 더 이해하기 쉽다.
이전 버전과 다른 점은 reverse + reduce -> reduceRight로 바꿔주었다.

예를 들어 미들웨어가 A, B, C가 있을 때를 생각해보자.

  1. reduceRight를 이용했으니
    맨 끝의 인자인 C가 먼저 순회돼서
    C -> reducer가 dispatch로서 리턴된다.
// 정확히는 이런 함수
action => { 
    do something logic of C // 미들웨어 C의 로직을 타고
    store.dispatch(action); // reducer에게 넘김
    // 추가 로직...
};
  1. 리턴된 C -> reducer는 다음 미들웨어인 B의 next 인자로 들어가게 된다.
    그러면 B -> C -> reducer가 리턴된다.
// 정확히는 이런 함수
action => {
    do something logic of B // 미들웨어 B의 로직을 타고
    passActionToC(action); // 미들웨어 C에게 넘김 (passActionToC는 next입니다.)
    // 추가 로직...
};
  1. 리턴된 B -> C -> reducer는 다음 미들웨어인 A의 next 인자로 들어가게 된다.
    그러면 A -> B -> C -> reducer가 리턴된다.
// 정확히는 이런 함수
action => {
    do something logic of A // 미들웨어 A의 로직을 타고
    passActionToB(action); // 미들웨어 B에게 넘김 (passActionToB는 next입니다.)
    // 추가 로직...
};
  1. 완성된 dispatch는 A -> B -> C -> reducer의 흐름을 갖는다.

끝에서부터 차근차근 이어주는 느낌이라 더 직관적이지 않은가?
아마 실제 구현은 reduceRight를 사용하지 않았을까 싶다.

더 자세한 동작 원리
출처 https://lunit.gitbook.io/redux-in-korean/advanced/middleware

profile
함수형 프로그래밍, 자바스크립트에 관심이 많습니다.

0개의 댓글