미들웨어가 없다면?
미들웨어는 리덕스를 이용하는데 필수적인 요소는 아니다. 다만 미들웨어를 통해서 리덕스를 좀 더 편리하게 사용할 수 있다. 기술의 필요성을 직관적으로 이해하려면 해당 기술이 없었을 때를 상상하거나, 실제 구현해보면 좋다고 한다.
실제 미들웨어가 없이 아래의 동작을 구현해보자.
- Dispatch된 Action을 로깅한다.
- Action이 Reducer로 전달되어서 처리된 후, state를 로깅한다.
Solution 1. Logging Manually
const increaseAction = increaseCounter();
console.log("dispatching", increaseAction);
store.dispatch(increaseAction);
console.log("next state", store.getState());
가장 단순한 방법은 매번 액션을 Dispatch 하기 전후로 직접 log를 출력하는 것
-> 매번 액션을 Dispatch 하는 코드마다 위와 같은 반복적인 코드를 작성하는 것은 바람직하지 않아보인다.
Solution 2. Wrapping Dispatch
두번째 방법은 store.dispatch 메서드를 감싸는 함수를 만들고, 그 함수 안에서 로깅 동작을 추가하는 것이다.
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
첫 번째 방법보다는 발전했지만 단점으로는 store.dispatch를 사용하는것이 아니라, 매번 dispatchAndLog라는 함수를 따로 import 해서 사용해야 한다. (실제 나중에 React-Redux등의 useDispatch와 같은 형태로 사용하려면 더 까다로워진다.)
Solution 3. Monkeypatching Dispatch
세번째 방법은 store.dispatch 메서드를 몽키패칭 하는 것이다. 몽키패칭이란, 라이브러리나, 프레임워크단의 코드의 동작을 직접 수정해서 사용하는 것을 의미한다. 이 경우에는 store.dispatch 함수를 수정해서 활용할 수 있다.
const originDispatch = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
const result = originDispatch(action)
console.log('next state', store.getState())
return result
}
장점
- 매번 로깅하는 코드를 작성하지 않아도 된다.
- 디스패치를 사용하는 입장에서 원래의 디스패치 메서드가 아닌 Wrapping 한 디스패치 함수를 import해서 사용하는데에 주의를 기울이지 않아도 된다.
따라서 가지 솔루션 중 가장 좋은 방법이라고 생각할 수 있다. ✔ 단, 1가지 기능만 추가할 경우에만 가장 좋은 방법이다.
Q. 여러개의 기능이 필요하다면?
만약 디스패치 메서드에 로깅 외에, try, catch 구문을 이용해 예외가 발생했을 시 error 로그를 출력하는 동작을 추가하려면 어떻게 하는 것이 좋을까
function patchStoreToAddLogging(store) {
const 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) {
const next = store.dispatch;
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('에러 발생', err);
throw err
}
}
}
// enhance
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
위와 같이 각각의 기능에 맞게 patching하는 함수를 작성한 후, 직접적으로 개별 함수를 실행해줘야 한다.
위 방식의 단점
- 함수 내에서 store.dispatch 메서드를 직접 수정하고 있기에 사이드 이펙트를 발생시켜서 추후 프로그램의 동작을 예측하기 어렵게 만들기에 유지보수에 좋지 않다.
- patching하는 함수들을 특정 위치에서 직접 개별적으로 모두 실행해줘야 한다.
Solution 4. Hiding Monkeypatching
위 문제를 해결하기 위한 방법으로 몽키패칭을 적용하는 부분을 별도 함수로 분리하는 방법이 있다. 여러개의 패칭 함수를 연결해서 수행하기 위해서, dispatch를 바로 함수 안에서 수정하는 게 아니라
- 패칭함수에서는 wrapping된 dispatch 함수를 return 한다.
- 몽키패칭을 적용시켜주는 함수에서는 return된 dispatch함수를 store.dispatch 에 패칭한다.
위의 과정을 수행해주면
function logger(store) {
const next = store.dispatch;
return function dispatchAndLog(action) {
console.log('dispatching', action)
const result = next(action)
console.log('next state', store.getState())
return result
}
}
function crashReporter(store) {
const next = store.dispatch;
return function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('에러 발생', err);
throw err
}
}
}
function applyMiddlewareByMonkeypatching(store, middlewares) {
copiesOfMiddlewares = [...middlewares];
// middleware는 순차적으로 실행되며,
// 마지막 middleware는 원래의 store.dispatch를 호출해줘야 한다.
// dispatch 함수를 patching하는 과정은 가장 끝 미들웨어부터 이루어져야 한다.
// 따라서, 역순으로 정렬시킨 후 patching 한다.
copiesOfMiddlewares.reverse()
// store.dispatch 메서드를 middleware 함수의 return값으로 변경한다.
copiesOfMiddlewares.forEach(middleware => (store.dispatch = middleware(store)))
}
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
위와 같이 하면 명시적으로 기존의 문제점을 해결할 수 있다. 하지만, 결국 몽키패칭을 수행하는 부분을 별도의 함수로 추출해서 숨기기만 했을 뿐, 내부적으로 몽키패칭을 수행하고 있다는 문제점은 그대로 남아있다.
Solution 5. Remove Monkypatching
몽키패칭을 수행해서 store.dispatch 를 직접 변경시키는 것에 대한 문제점
- 결국 라이브러리단의 코드의 동작을 직접 수정해버린다는 위험성
- store.dispatch 가 지속적으로 변하기 때문에, 그로 인한 버그가 발생하기 쉽다는 취약점
- 그리고 기존의 store.dispatch 를 덮어씌워버리기 때문에, 추후 만약 기존의 dispatch 메서드가 필요한 순간이 발생할 경우 이에 대응하기 어렵다는 점
몽키패칭을 수행하는 방법을 선택한 근본적인 이유
여러개의 함수들이 순차적으로 store.dispatch에 특정한 동작을 추가하기 위해서 몽키패칭을 수행했다. 하지만 조금만 더 생각해보면, 여러개의 함수들이 순차적으로 동작을 추가하기 위해서는 몽키패칭 외에 다른 방법을 선택할 수 있다.
바로, 미들웨어에서 patching된 함수를 리턴하고, 이를 다음 미들웨어의 인자로 전달하는 방식이다.
- 기존 방식
- 미들웨어 함수에 store 만 인자로 전달한다.
- 미들웨어에서는 원하는 동작을 수행한 뒤 store.dispatch 를 호출해준다.
- 새로운 방식
- 미들웨어 함수에 store와, next를 인자로 전달한다
- 미들웨어에서는 원하는 동작을 수행한 뒤 next 함수를 호출해준다.
- next는 미들웨어가 동작을 수행한 뒤 호출할 함수를 의미한다.
function logger(store) {
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
const result = next(action)
console.log('next state', store.getState())
return result
}
}
}
// arrow function style
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
// apply middleware
function applyMiddleware(store, middlewares) {
copiesOfMiddlewares = [...middlewares];
// middleware는 순차적으로 실행되며,
// 마지막 middleware는 원래의 store.dispatch를 호출해줘야 한다.
// dispatch 함수를 patching하는 과정은 가장 끝 미들웨어부터 이루어져야 한다.
// 따라서, 역순으로 정렬시킨 후 patching 한다.
copiesOfMiddlewares.reverse()
// dispatch 변수를 선언하고, 초기값으로는 store.dispatch를 할당한다
let dispatch = store.dispatch;
// dispatch 변수의 값을 middleware 함수의 리턴값으로 재할당한다.
copiesOfMiddlewares.forEach((middleware) => {
dispatchEnhancer = middleware(store);
dispatch = dispatchEnhancer(dispatch)
});
return {...store, dispatch}
}
applyMiddleware(store, [logger, crashReporter])
'Library & Framework > Redux' 카테고리의 다른 글
Redux-Saga (0) | 2022.10.29 |
---|---|
Redux Middleware / DevTools (0) | 2022.10.29 |
Redux (0) | 2022.10.29 |