
이번 아티클에서는 dispatch
함수를 호출하면 비동기 API를 호출하거나 로깅을 수행하는 middleware 대해서 살펴보겠습니다. middleware가 어떻게 동작하는지, 실제 middleware는 어떻게 만드는지 살펴볼것이므로 이번 아티클을 읽고나면 middleware 오픈소스(redux-thunk, redux-saga등)의 분석뿐만 아니라, 나만의 middleware도 만들수 있을것입니다.
본격적으로 middleware를 살펴보기에 앞서서 이전 아티클에서 createStore
함수를 분석할때 생략했던 Enhancer를 설명하는것으로 이번 아티클을 시작해보겠습니다.
Enhancer

앞서 살펴본것처럼 store
는 createStore
함수에 reducer
와 preloadedState
를 넣어 실행하면 얻을 수 있습니다. 그리고 이렇게 얻은 store
의 getState
, dispatch
, subscribe
함수를 이용해 상태를 관리 할 수 있습니다.

그런데 createStore
함수는 세번째 인수로 createStore
함수로 전달하는 인자나 반환하는 함수들의 동작을 수정할 수 있도록 해주는 enhancer
함수를 받을 수 있습니다.
// 실제 createStore 함수내부
if (typeof enhancer === "function") {
return enhancer(createStore)(reducer, preloadedState)
}
실제로 creatStore
함수가 enhancer
함수를 받을 경우 위와 같이 enhancer
함수에 createStore
함수를 넣어 실행한뒤 반환된 강화된 creatStore
함수에 기존 인자를 넣어 실행한뒤 반환하는것을 볼 수 있습니다.
실제
createStore
함수의 타입 정의를 살펴보면,preloadState
는 생략이 가능하므로 두번째 인자로enhancer
함수를 전달해 줄수도 있습니다.export function createStore< S, A extends Action, Ext extends {} = {}, StateExt extends {} = {} >( reducer: Reducer<S, A>, enhancer?: StoreEnhancer<Ext, StateExt> ): Store<S, A, UnknownIfNonSpecific<StateExt>> & Ext
createStore에 enhacer를 적용하기
enhacer
함수는 store
로 들어오는 인자들(reducer
, state
등)과 store
가 반환하는 함수 (dispatch
, subscribe
, getState
등)의 프록시 함수를 정의할 수 있습니다. reducer
와 dispatch
를 강화하는 예제를 한번 살펴보겠습니다.
import { createStore } from "redux";
const enhancer = (createStore) => {
return (reducer, state) => {
// 넘겨받은 reducer 함수가 실행되는 시간을 측정할 수 있도록 reducer 함수를 새롭게 정의합니다.
const newReducer = (state, action) => {
const start = performance.now()
const newState = reducer(state, action)
const end = performance.now()
const diff = round(end - start)
console.log('reducer 실행시간:', diff)
return newState
}
const store = createStore(newReducer, state)
// dispatch의 실행여부를 알수 있도록 dispatch 함수를 새롭게 정의합니다.
const newDispatch = (action) => {
console.log(`dispatch가 발생했습니다! action:${action} `)
return store.dispatch(action)
}
return {
...store
dispatch: newDispatch
}
}
}
const store = createStore(rootReducer, undefined, enhancer);
여러 enhancer를 동시에 사용하기
위 예제처럼 하나의 enhacer
함수만 사용하는 경우도 있지만, 이미 만들어진 여러 enhancer
함수를 적용하기 위해서 enhacer
함수를 여러번 사용해야할수 있습니다. 앞서 보았던것처럼 enhancer
함수는 createStore
함수를 받아 강화된 createStore
함수를 반환하기 때문에 이전 enhancer
함수의 결과를 다음 enhancer
에 넣어 실행하면 여러 enhacer
함수를 동시에 사용할 수 있게됩니다. 예제코드와 함께 어떤방식으로 여러 enhacer
를 적용하는지 살펴봅시다.
import { createStore, compose } from "redux"
import rootReducer from "./reducer"
import {
sayHiOnDispatch,
includeMeaningOfLife,
checkRedcuerFnTime,
} from "./exampleAddons/enhancers"
const composedEnhancer = compose(
sayHiOnDispatch,
includeMeaningOfLife,
checkRedcuerFnTime
)
const store = createStore(rootReducer, undefined, composedEnhancer)
export default store
코드를 보면 compose
함수에 여러개의 enhacer
함수를 넣어주고 있는데, 앞서 언급한것처럼 이전 enhancer
함수의 결과를 다음 enhancer
함수에 넣어 실행할 수 있도록 연결된 함수를 만들어주는것입니다. 즉 const composedEnhancer = (...arg)=>sayHiOnDispatch(includeMeaningOfLife(checkRedcuerFnTime(...arg)))
와 동일하다고 볼수 있습니다.
따라서 composedEnhancer
함수는 createStore
함수를 받아 여러 enhacer
함수가 적용되어 강화된 createStore
함수를 반환하므로 원본 createStore
함수의 enhacer
함수로 사용할수 있습니다. compose
함수를 분석하면서, 어떻게 이러한 동작을 하는지 살펴보겠습니다.
export default function compose(...funcs: Function[]) {
return funcs.reduce(
(a, b) =>
(...args: any) =>
a(b(...args))
)
}
이 함수는 배열로 받은 인자에 reduce
함수를 적용한 값을 반환합니다. 이 함수는 하나의 함수를 반환하는데 이 함수를 실행하면 첫번째 함수부터 반환한 함수에 넘긴 인자를 인자로 하여 실행한뒤, 결과를 두번째 함수에 넘겨 실행하는 과정을 마지막 함수까지 반복한뒤 결과를 반환하게 됩니다. 즉 인자로 넘긴 함수들을 연이어 실행하여 반환하는것입니다.
위 예제를 적용해보면 다음과 같습니다. reduce의 초기값이 없으므로 0번 index는 초기값으로 설정하고 1번 요소부터 살펴보겠습니다.
a
는 첫번째 요소이자 초기값이자 이전값인checkRedcuerFnTime
함수,b
는includeMeaningOfLife
함수가 될것입니다. 즉reduce
함수가 반환하는 값은(...args:any) => checkRedcuerFnTime(includeMeaningOfLife(...args))
입니다.a
는 이전 값인(...args:any) => checkRedcuerFnTime(includeMeaningOfLife(...args))
,b
는 세번째 요소인checkRedcuerFnTime
함수가 될것입니다. 즉reduce
함수가 반환하는 값은(...args: any) =>((...args:any) => checkRedcuerFnTime(includeMeaningOfLife(...args)))(checkRedcuerFnTime(...args))
입니다.
enhancer는 createstore를 받아서 createstore를 반환하므로, 위 함수를 통해 여러개의 enhancer를 합칠수 있는것입니다.
조금 복잡할수는 있겠지만, 가능한 위 로직을 이해하고 넘어가시는것을 추천드립니다. 그래야 다음에 설명할 내용도 쉽게 이해하실수 있기 때문입니다.
Middleware
앞서 살펴본 enhancer
함수는 store의 모든 요소를 수정할 수 있는데 일반적으로는 dispatch의 기능을 강화하는것만으로도 충분합니다. 이때 등장하는것이 바로 middleware입니다. middleware는 enhancer
함수의 일종으로 모든 기능을 강화하지 않고 오로지 dispatch만을 강화하는데 사용되는 도구입니다.
const middleware =
({ dispatch, getState }) =>
next =>
action => {
/* ... 특정 작업을 수행합니다. */
return next(action)
}
const middlewareEnhancer = applyMiddleware(
middleware1,
middleware2,
middleware3
)
const store = createStore(rootReducer, middlewareEnhancer)
일반적으로 middleware의 타입은 위와 같으며 이러한 middleware를 applyMiddleware
함수에 넣어 사용하게 됩니다. 따라서 middleware의 동작을 분석하는것은 곧 applyMiddleware
함수를 분석하는것과 동일하므로 applyMiddleware
함수를 살펴보겠습니다.
applyMiddleware분석
앞서 예제코드에서 확인한것처럼 applyMiddleware
함수는 enhacer
함수의 타입을 리턴해야한다는점과, 여러 middleware를 순서대로 결합해주어야한다는 사실을 기억하면서 분석을 시작해보겠습니다.
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer {
return createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch: Dispatch = () => {}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch,
}
}
}
먼저 middlewares.map(middleware => middleware(middlewareAPI))
로직을 통해 인자로 받은 middleware에 새로운 getState
함수와 dispatch
함수를 넣어 실행하면 (next)=>(action)=>{return next(action)}
과 같은 요소 3개가 배열에 포함될것입니다.
이 배열을 앞서 살펴본 compose
함수에 넣게되면 첫번째 미들웨어부터 순서대로 미들웨어를 실행한 결과를 반환하는 함수를 생성하게 되고, 이 함수에 store.dispatch
를 넣어 실행하면 첫번째 요소의 경우 next
인자로 store.dispatch
가 포함되어 콜백함수가 반환되며, 해당 콜백함수를 인자로 두번째 요소의 함수가 실행되는 방식으로 결합되므로 next를 호출하면 다음 미들웨어 혹은 dispatch로 이동할수 있게됩니다.
따라서 middleware가 적용된 dispatch
함수에 action을 넣어 실행하면 마지막 요소부터 첫번째 요소까지 미들웨어가 실행되며 첫번째 요소의 미들웨어에서 next
를 호출하면 실제 dispatch
를 실행하게됩니다.
redux-thunk
middleware에 대해 이해했다면 이제 오픈소스 middleware 라이브러리중 하나인 Redux-Thunk를 분석해보겠습니다.
사용법
Redux-Thunk의 사용 방식을 예제코드와 함께 알아보겠습니다.
// thunk 액션 생성자 함수
export const fetchTodoById = todoId => async dispatch => {
const response = await client.get(`/fakeApi/todo/${todoId}`)
dispatch(todosLoaded(response.todos))
}
// 사용 컴포넌트
function TodoComponent({ todoId }) {
const dispatch = useDispatch()
const onFetchClicked = () => {
dispatch(fetchTodoById(todoId))
}
}
dispatch 이전에 비동기 호출을 하기 위해 dispatch를 인자로 받는 함수를 dispatch에 넣어 실행합니다. 그리고 비동기나 특정 작업을 수행한뒤에 해당 함수 내부에서 인자로 넘어온 dispatch를 사용해줍니다. 이렇게 사용하게되면 특정 작업수행후 disaptch를 실행할수 있게 됩니다.
코드 분석
middleware를 사용하였을때 Redux-Thunk에서 가져올수 있는 코드는 extraThunkArg
를 제외한다면 아래와 같습니다.
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === "function") {
return action(dispatch, getState)
}
return next(action)
}
앞서보았던 middleware의 형태와 동일한것을 알 수 있습니다. 콜백함수의 조건문을 통해 action으로 객체가 아닌 함수를 넘겼을때, a
ction함수를 실행하게 됩니다. 이후 주도권이
action함수로 넘어가므로 비동기작업이나 원하는 작업을 수행한뒤, 다시
dispatch에 실제
action`객체를 넣어 실행하면, 주도권을 다시 Redux에 돌려주고 상태가 store에 반영됩니다.
마치며
이번 아티클에서는 Redux의 middleware 개념을 살펴보았습니다. 이번 내용을 바탕으로 redux-thunk뿐만 아니라 redux-saga와 같은 미들웨어 라이브러리도 함께 분석해보면, 미들웨어의 활용 방식에 대한 이해에 큰 도움이 될 것입니다.
이 아티클을 마지막으로 Redux Deep Dive 시리즈를 마무리합니다. 이번 시리즈를 바탕으로, 여러분이 사용 중인 상태 관리 라이브러리를 직접 분석해보는 것도 좋은 학습 방법이 될 것입니다.
참고 자료
Redux Fundamentals, Part 4: Store Redux enhancer Exploring ApplyMiddleware: How it works behind the scenes