우아한 테크러닝 3기

✔ 우아한 테크러닝 3기: React & TypeScript 5회차

seungmin2 2020. 9. 19. 19:03

✌ 9월15일 (화) 우아한 테크러닝 3기 5회차 강의 정리

🚀 5회차 강의 목표

  • javascript로 만든 Redux 리뷰
  • Redux의 비동기
  • Redux 미들웨어 알아보기

💻 Redux 리뷰

import { createStore } from "./redux";

function reducer(state = { counter: 0 }, action) {
  switch (action.type) {
    case "inc":
      return {
        ...state,
        counter: state.counter + 1
      };
    default:
      return { ...state };
  }
}

const store = createStore(reducer);

store.subscribe(() => {
  //Object {counter: 1}
  //Object {counter: 2}
  console.log(store.getState());
});

store.dispatch({
  type: "inc"
});

store.dispatch({
  type: "inc"
});
  • redux.js
export function createStore(reducer) {
  let state;
  const listeners = [];
  const getState = () => ({ ...state });

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

  const subscribe = (fn) => {
    listeners.push(fn);
  };

  return {
    getState,
    dispatch,
    subscribe
  };
}
  • index.js에서 이뤄지는 모든 동작 즉, reducer따로, store 만드는것 따로, subscribe 따로, dispatch가 각각 따로 일어나는데 실제로 모든 동작은 동기적으로 작동한다.
    그렇기 때문에 순서가 완전히 보장이된다.

  • 그래서 reducer순수함수 여야만한다. (원칙)
    순수함수란 외부와 아무런 dependency없이 내부적으로 아무런 side effect 없이 작동하는 함수이다.

    멱등성: 인풋이 똑같으면 아웃풋도 무조건 같아야되는 성질

🙄 순수하지 않은 작업은 뭘까??

  • 실행될때마다 결과가 일정치 않은 작업들을 순수하지 않는 작업이라한다.
    대표적으로 비동기 작업이고 그중애서도 대표적인 것은 API호출이다.
    그렇기 때문에 결과 예측이 안된다.

🙄 그래서 순수하지 않는 상황은 어떻게 다룰까??

  • 현재 상태에서는 API를 다루는 것은 불가능하다.
  • index.js
...
// api 받는 함수
function api(url, cb) {
  setTimeout(() => {
    cb({ type: "응답이야", data: [] });
  }, 2000);
}
...
function reducer(state = { counter: 0 }, action) {
  switch (action.type) {
    case "inc":
      return {
        ...state,
        counter: state.counter + 1
      };
    case "fetch-user":
      api("/api/v1/users/1", (users) => {
        return { ...state, ...users };
      });
      break;
    default:
      return { ...state };
  }
}
...
store.dispatch({
  type: "fetch-user"
})
  • Redux는 모든 로직을 동기적으로 처리하기 때문에 값을 기다리지 않는다.(setTimeout) 그렇기 때문에 reducer가 호출되고 api가 호출되고 반환을 하지만 현재 상태에서는 반환한 상태가 없게 된다.
    그렇기 때문에 store의 상태를 바꾸는 것은 불가능하게 된다.

따라서 redux는 비동기 작업을 할 때 미들웨어을 사용한다.

🚀 Redux의 미들웨어

  • 아래와 같은 형태들로 만들 수 있다.
const myMiddleware = (store) => (dispatch) => (action) => {
  dispatch(action);
};

// 일반 함수
function yourMiddleware(store) {
  return function (dispatch) {
    return function (action) {
      dispatch(action);
    };
  };
}

function ourMiddleware(store, dispatch, action) {
  dispatch(action);
}
  • myMiddlewareyourMiddleware는 문법만 다르고 같은 형태이고 ourMiddleware은 함수가 하나이다.

  • 이 세 함수의 공통점은 실행하는 코드가 같다.

  • 다른점은 myMiddleware,yourMiddlewareourMiddleware의 다른점은 중첩되어 있는 지점이 다르다.

    인자가 n개인 함수를 인자를 각각 하나씩 쪼갠 함수를 분리하는 기법을 함수형 프로그래밍에서는 커링이라고 한다.

// 커링을 이용한 함수를 이렇게 실행 할 수 있다.
myMiddleware(store)(store.dispatch)({ type: "inc" }); // Object {counter: 1}
yourMiddleware(store)(store.dispatch)({ type: "inc" }); // Object {counter: 2}
  • ourMiddleware 호출 할 땐 인자를 한꺼번에 전달한다.
ourMiddleware(store, store.dispatch, { type: "inc" }); // Object {counter: 3}

🤔 커링을 사용해서 미들웨어을 사용하는 이유는 뭘까??

  • 아래와 같이 어떤 타입이 들어왔나 로그를 찍고싶을 때가 있다.
console.log('action -> { type: "INC" }');
myMiddleware(store)(store.dispatch)({ type: "inc" }); 
  • 그치만 필요할때 마다 작성해줘야 되기 때문에 굉장히 번거롭다.
  • Redux의 공식문서를 참고해보자.

🌊 디스패치 감싸기

function dispatchAndLog(store, action) {
    console.log('dispatching', action);
    store.dispatch(action);
    console.log('next state', store.getState());
}
// 로깅함수 호출
dispatchAndLog(store, { type: "inc" });
dispatchAndLog(store, { type: "inc" });
  • 이런식으로 로깅을 함수로 뽑아낼 수 있다.

  • 하지만 위 상항도 하드코딩이 되어있기 때문에 코드를 수정하는 것은 매우 좋지 못한 방법이다.

🌊 디스패치 몽키패칭하기

  • store.dispatchnext라는 변수를 만들어서 함수 자체를 넣어준다. (원본을 넣는다.)
  • 그리고 store.dispatchdispatchAndLog 함수로 바꿔준다.
  • 실행시간에 필요한 것으로 바꿔주고 필요하지 않아졌을 때 원래대로 복구시켜준다.
let next = store.dispatch; 
// 바깥쪽 store.dispatch 에서는 로깅 찍히는 작업 바꿔준다. 
store.dispatch = function dispatchAndLog(action) { 
    console.log('dispatching', action); 
    // 원래의 것으로 dispatch 해준다. 클로저로 가지고 있다. 
    let result = next(action); 
    console.log('next state', store.getState()); 
    return result; 
};
  • 이제 위와 같이 하면 dispatchAndLog 함수를 사용하지 않고 dispatch를 할 수 있게 되었다.
  • 하지만! 만약 dispatch에 이런 함수 변환(위 처럼 함수를 바꿔치기 하는 것. 즉, 로깅하는 함수와 또 다른 함수)을 두 개 이상 적용할 때는 어떻게 될까??

🌊 디스패치 두 개 이상 몽키패칭하기

  • 로깅 이 외의 오류를 잡을 때를 콘솔에 값을 출력하는 로직을 추가시켜 보자.
  • 여기서 중요한 것은 안에 있는 코드가 어떤 코드인지는 중요하지 않다.
    중요한 점은 함수가 함수를 리턴하고 있고, 바깥 함수와 안쪽 함수가 분리되어있다는 점이다.

    patchStoreToAddLoggingdispatchAndLog
    patchStoreToAddCrashReportingdispatchAndReportErrors

function patchStoreToAddLogging(store) { 
    let next = store.dispatch; 
    store.dispatch = function dispatchAndLog(action) { 
        console.log('dispatching', action);
        let result = next(action); 
        console.log('next state', store.getState()); 
        return result; 
    }; 
} 

function patchStoreToAddCrashReporting(store) {
  let next = store.dispatch;
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action);
    } catch (err) {
      console.error("Caught an exception!", err);
      Raven.captureException(err, {
        extra: { action, state: store.getState() }
      });
      throw err;
    }
  };
}

📌 여기서 잠깐 커링의 예시

  • add1함수는 사용자가 최종 계산에 개입할 여지가 전혀 없다. (10 + 20을 변화시킬 수 없다.)
const add1 = function (a, b) { 
    return a + b; 
}; 

const add2 = function (a) { 
    return function (b) {
        return a + b; 
    }; 
}; 

add1(10, 20); 
add2(10)(20);
  • add2함수는 사용자가 개입할 여지가 충분히 존재한다.
  • 그렇기 때문에 지연효과를 줄 수 있다.
// 사용자 쪽에서 작업을 할 수있다. 
const addTen = add2(20); 
// do some.. 
addTen(20); 
addTen(120);
  • 결국 커링은 사용자한테 인자와 인자 사이에 개입할 수 있는 여지를 열어 주는 것이다.

🌊 몽키패칭 숨기기

  • 여기서 본격적인 커링의 모양이 등장한다.
  • dispatchAndLoglogger함수로 감싸고 dispatchAndLog를 리턴한다.
function logger(store) {
  let next = store.dispatch;

  // 앞에서:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  };
}
  • 여기서 사용자인 리덕스가 미들웨어로 사용자가 만든 함수를 조작하고 싶은데 add1(10, 20); 이런식으로 만들면 조작할 수 없으니 리덕스가 개입해서 원하는 형태로 재구성할 수 있는 여지를 주는 커링 테크닉을 사용하는 것이다. add2(10)(20);
  • 각각의 미들웨어를 적용시키는 applyMiddlewareByMonkeypatching 함수를 아래와 같이 작성할 수 있다.
  • Redux 안에 실제 몽키패칭을 적용할 수 있게 돕는 헬퍼를 제공할 수 있다.
function applyMiddlewareByMonkeypatching(store, middlewares) { 
    middlewares = middlewares.slice(); 
    middlewares.reverse(); 
    // 각각의 미들웨어로 디스패치 함수를 변환 
    middlewares.forEach(middleware => store.dispatch = middleware(store) ); 
} 

// 여러 미들웨어를 적용 
applyMiddlewareByMonkeypatching(store, [logger, crashReporter]);
  • 하지만 이건 몽키패칭을 숨기기만하고 아직 사용중이다.
  • 몽키패칭을 제거하기 위해서 아래와 같이 사용할 수 있다.
// 바깥 
function logger(store) { 
    // 두번째 함수 
    return function wrapDispatchToAddLogging(next) { 
        // 마지막 함수 
        return function dispatchAndLog(action) { 
            // 결국 할려는 것 
            console.log('dispatching', action); 
            let result = next(action); 
            console.log('next state', store.getState()); 
            return result; 
        }; 
    } 
}
  • 미들웨어가 next() 디스패치 함수를 store 인스턴스에서 읽어오는 대신 매개변수로 받을 수 있다.
  • 화살표 함수를 이용해 커링을 간단하게 나타낼 수 있다.
const logger = store => next => action => { 
    console.log('dispatching', action); 
    let result = next(action); 
    console.log('next state', store.getState()); 
    return result; 
};
  • 결국 최종적으로 마지막 안쪽 함수에만 쓰면된다.
    등록된 미들웨어들을 순서대로 연결시켜 놓은 구조를 만들 수 있게 리덕스가 할 수 있게 열어놓는 구조를 만들기 위해서 커링 테크닉을 사용한 것이다.

📚 Redux에 미들웨어 추가하기

  • 미들웨어를 추가해준다.
  • 위 경우에서는 setTimeout를 사용한 비동기적 함수는 실행되지 않았다.
// index.js 
const logger = (store) => (next) => (action) => { 
    console.log("logger:", action.type); 
    next(action); 
}; 

const monitor = (store) => (next) => (action) => { 
    setTimeout(() => { 
        console.log("monitor:", action.type); 
        next(action); 
    }, 2000); 
};
  • 미들웨어를 받는 부분을 작성해준다.
    middlewares는 미들웨어를 배열로 받는다.
// redux.js 
export function createStore(reducer, middlewares = []) { 
    ... 
    middlewares = Array.from(middlewares).reverse(); 
    let lastDispatch = store.dispatch; 
    middlewares.forEach((middleware) => { 
        lastDispatch = middleware(store)(lastDispatch); 
    }); 
    return { ...store, dispatch: lastDispatch }; 
    ... 
}
  • middlewares.reverse();를 해주는 이유는 middleware(store)(lastDispatch) 형태로 호출되며 반환값은 계속해서 앞에 미들웨어에 전달된다.
    그래서 reverse로 뒤집어서 가장 뒤인 안쪽에서 부터 바깥쪽으로 dispatch가 전달된다. 그렇기 때문에 최종 실행되는 dispatch 함수는 기존 맨 처음의 storedispatch 함수가 되게 된다.
  • 결과적으로 아래와 같이 콘솔이 찍히고, 미들웨어를 순서를 변경해도 그대로 동기적으로 실행된 결과가 나온다.

모든 소스코드는 Github 참고
김민태님 Gist 참고

🚀 5회차 후기

이번 미들웨어 수업은 이해하기가 어려웠다. 그래도 블로깅을 하면서 이해해볼려고 노력했다..😥
공부가 많이 필요한 부분인 거 같다.
바로바로 블로깅을 했어야 했지만 생각보다 시간적 여유가 부족한거 같다.. 그래도 꼭 8회차까지 다써야겠다.