✔ 우아한 테크러닝 3기: React & TypeScript 5회차
✌ 9월15일 (화) 우아한 테크러닝 3기 5회차 강의 정리
🚀 5회차 강의 목표
- javascript로 만든 Redux 리뷰
- Redux의 비동기
- Redux 미들웨어 알아보기
💻 Redux 리뷰
- 2회차때 만든 리덕스를 사용하여
store
와reducer
생성 해준다. - 자세한 내용은 위 링크 참조 🙏
index.js
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);
}
myMiddleware
와yourMiddleware
는 문법만 다르고 같은 형태이고ourMiddleware
은 함수가 하나이다.이 세 함수의 공통점은 실행하는 코드가 같다.
다른점은
myMiddleware
,yourMiddleware
와ourMiddleware
의 다른점은 중첩되어 있는 지점이 다르다.인자가 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.dispatch
를next
라는 변수를 만들어서 함수 자체를 넣어준다. (원본을 넣는다.)- 그리고
store.dispatch
를dispatchAndLog
함수로 바꿔준다. - 실행시간에 필요한 것으로 바꿔주고 필요하지 않아졌을 때 원래대로 복구시켜준다.
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
에 이런 함수 변환(위 처럼 함수를 바꿔치기 하는 것. 즉, 로깅하는 함수와 또 다른 함수)을 두 개 이상 적용할 때는 어떻게 될까??
🌊 디스패치 두 개 이상 몽키패칭하기
- 로깅 이 외의 오류를 잡을 때를 콘솔에 값을 출력하는 로직을 추가시켜 보자.
- 여기서 중요한 것은 안에 있는 코드가 어떤 코드인지는 중요하지 않다.
중요한 점은 함수가 함수를 리턴하고 있고, 바깥 함수와 안쪽 함수가 분리되어있다는 점이다.patchStoreToAddLogging
와dispatchAndLog
patchStoreToAddCrashReporting
와dispatchAndReportErrors
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);
- 결국 커링은 사용자한테 인자와 인자 사이에 개입할 수 있는 여지를 열어 주는 것이다.
🌊 몽키패칭 숨기기
- 여기서 본격적인 커링의 모양이 등장한다.
dispatchAndLog
를logger
함수로 감싸고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
함수는 기존 맨 처음의store
의dispatch
함수가 되게 된다.- 결과적으로 아래와 같이 콘솔이 찍히고, 미들웨어를 순서를 변경해도 그대로 동기적으로 실행된 결과가 나온다.
🚀 5회차 후기
이번 미들웨어 수업은 이해하기가 어려웠다. 그래도 블로깅을 하면서 이해해볼려고 노력했다..😥
공부가 많이 필요한 부분인 거 같다.
바로바로 블로깅을 했어야 했지만 생각보다 시간적 여유가 부족한거 같다.. 그래도 꼭 8회차까지 다써야겠다.