서론
Redux 톺아보기를 공부하며 “액션을 dispatch만 했는데, Redux는 어떻게 미들웨어를 실행하는 걸까?“라는 질문에 대한 답을 나름대로 내려보는 글이다. 이에 대한 답을 하기 위해서는 Redux의 내부 동작 원리, 특히 applyMiddleware 함수와 compose 함수를 이해해야 하는 것 같다.
이번 글에서는 Redux의 dispatch와 미들웨어가 어떻게 연결되고 동작하는지 단계별로 설명한다.
기본 dispatch의 동작
Redux의 기본 dispatch는 단순하다.
dispatch는 액션을 리듀서로 전달하고, 상태를 업데이트하는 역할을 한다.
const store = createStore(reducer);
store.dispatch({ type: 'INCREMENT' });
// 기본 dispatch는 단순히 리듀서로 액션을 전달한다.
하지만 Redux에 미들웨어를 추가하면, 이 기본 dispatch는 미들웨어 체인을 거치는 확장된 dispatch로 대체된다.
applyMiddleware가 확장된 dispatch를 만드는 원리
Redux의 applyMiddleware는 기존 dispatch를 Middleware 체인으로 감싸는 새로운 dispatch를 반환한다.
이 과정에서 Redux는 모든 미들웨어를 순서대로 연결하고, 액션이 체인을 따라 흐르도록 설정한다.
아래는 applyMiddleware의 코드다.
function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState);
let dispatch = store.dispatch;
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action),
};
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
// 미들Middleware 체인을 통해 dispatch를 확장한다.
dispatch = compose(...chain)(store.dispatch);
return { ...store, dispatch };
};
}
먼저 chain은 middlewares(middleware들의 배열)을 map함수를 통해 기본 middlewareAPI를 첫번째 인자(store)에 추가한다. middleware는 currying 함수이기에 인자들이 전부 전달되기 전에는 실행되지 않는다. chain은 다시 compose함수를 거쳐 합성된 함수를 반환한다.
compose 함수로 미들Middleware 체인 연결
compose는 Redux에서 Middleware들을 합성하여 체인으로 연결하는 함수다. Middleware 체인은 각 Middleware가 다음 Middleware를 호출하며, 최종적으로 Redux의 기본 dispatch를 호출하는 구조로 동작한다.
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
이때 funcs는 미들Middleware 함수들의 배열이다. compose함수는 reduce를 사용해 함수를 순서대로 합성하여 중첩된 함수 체인을 생성하고 최종적으로 반환된 함수는 인자(args)를 받아 가장 마지막 함수부터 실행한다.
따라서 applyMiddleware가 반환하는 새로운 dispatch는 다음과 같은 구조로 동작한다.
dispatch = middleware1(storeAPI)(
middleware2(storeAPI)(
store.dispatch
)
);
Middleware 체인은 각 Middleware가 순서대로 호출되도록 연결되며, 체인의 마지막에는 Redux의 기본 dispatch가 있다.
dispatch가 호출되면, Middleware 체인의 첫 번째 함수부터 순차적으로 실행된다.
store.dispatch({ type: 'TEST_ACTION' });
라는 action이 dispatch되면, Middleware1이 실행되고 이에 Middleware1의 next(action)이 호출된다. 이것은 다음 Middleware인 Middleware2이다. Middleware2가 실행되면 마지막 Middleware이므로 Redux의 기본 dispatch 호출하여 리듀서로 액션을 전달한다.
예시를 통해 좀더 이해해보자면,
const loggerMiddleware = (storeAPI) => (next) => (action) => {
console.log("Logger Middleware 실행:", action);
return next(action);
};
const customMiddleware = (storeAPI) => (next) => (action) => {
console.log("Custom Middleware 실행:", action);
return next(action);
};
// Redux에서 스토어 생성 시 applyMiddleware 사용
const store = createStore(
reducer,
applyMiddleware(loggerMiddleware, customMiddleware)
);
// 디스패치 호출
store.dispatch({ type: 'TEST_ACTION' });
applyMiddleware는 아래와 같은 방식으로 새로운 dispatch를 반환한다. 이를 풀어서 보면 다음과 같은 내부 동작으로 구성된다.
먼저 미들웨어들이 storeAPI와 연결된다.
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => store.dispatch(action), // 초기 dispatch 참조
};
const chain = [loggerMiddleware(middlewareAPI), customMiddleware(middlewareAPI)];
이 단계에서 각 미들웨어는 storeAPI를 받아 next 함수로 연결된다. 또한 compose로 합성된 함수는 다음과 같을 것이다.
const dispatchWithMiddleware = loggerMiddleware(middlewareAPI)(
customMiddleware(middlewareAPI)(store.dispatch)
);
따라서 최종적으로 applyMiddleware를 통해 반환된 dispatch는 다음과 같은 형태일 것이다.
const dispatch = (action) => {
// Logger Middleware 실행
loggerMiddleware(middlewareAPI)(
// Custom Middleware 실행
customMiddleware(middlewareAPI)(
// 기본 dispatch 실행
store.dispatch
)
)(action);
};
그래서 action을 dispatch하면, 기본 dispatch가 아닌 반환된 dispatch를 통해 action이 실행되고, 그 과정은
1. loggerMiddleware 실행:
• action을 로깅하고 next(action) 호출.
2. customMiddleware 실행:
• action을 로깅하고 next(action) 호출.
3. Redux의 기본 dispatch 실행:
• 리듀서에 action 전달.
일 것이다.
결론
따라서 “액션을 dispatch만 했는데, Redux는 어떻게 미들웨어를 실행하는 걸까?“라는 질문에 대한 답은 middleware를 apply할 때 middleware로 감싸진 dispatch가 return되고, action이 dispatch되면 이 dispatch가 실행되기 때문이다.
이것이 가능한 이유는 middleware가 currying함수로 쓰여져 있기 때문이다. currying을 통해 인자를 한번에 받는 것이 아닌 순차적으로 받아 lazy execution이 가능하고, 함수를 합성하여 chain을 구성할 수 있기 때문이다. 인자를 순차적으로 받는다는 것이 처음에는 이해가 안됐는데 함수의 선언과 호출이 다름을 이해하고 선언될 때 힙에 로드되고 호출될 때 스택에 쌓인다는 점을 생각하면 currying 함수에 대한 이해에 더 도움이 될 것이다.
참고자료
'Library, Tool' 카테고리의 다른 글
Redux의 기본 개념 (2) | 2024.11.29 |
---|---|
TypeScript: type과 Interface의 차이 (4) | 2024.11.04 |
Frontend에서의 Bundler (0) | 2024.09.25 |
웹 성능을 높일 수 있는 bundler plugins (2) | 2024.09.11 |
Git의 동작원리 (0) | 2024.07.15 |