21. Redux 미들웨어의 이해
09편에서 미들웨어를 “액션이 리듀서에 도달하기 전 가로채는 지점"이라고 간단히 소개했습니다. Phase 5는 이 미들웨어를 직접 만들어보며 구조를 완전히 이해하고, 22~24편에서 다룰 Thunk·Saga·RTK Query가 모두 같은 미들웨어 메커니즘 위에 세워진 도구라는 것을 확인합니다.
학습 목표
- 미들웨어의 3중 함수 구조(
store => next => action => {})를 직접 작성할 수 있다. next(action)을 호출하지 않으면 액션이 리듀서에 도달하지 않는다는 것을 실습으로 확인할 수 있다.- 여러 미들웨어가 체인으로 연결되어 순차 실행되는 원리를 설명할 수 있다.
왜 미들웨어가 필요한가
07편에서 배운 리듀서의 규칙(순수 함수, 부수 효과 없음)을 다시 떠올려봅시다. API 호출, 로깅, 지연 실행 같은 부수 효과는 리듀서 안에 넣을 수 없습니다. 그렇다고 컴포넌트마다 로깅 코드를 반복해서 넣는 것도 비효율적입니다. 미들웨어는 **“액션이 dispatch되고 리듀서에 도달하기 전”**이라는 공통 지점에서 이런 부수 효과를 한 곳에 모아 처리할 수 있게 해줍니다.
flowchart LR dispatch["dispatch(action)"] --> mw1["미들웨어 1"] mw1 --> mw2["미들웨어 2"] mw2 --> reducer["리듀서"] reducer --> newState["새 상태"]
가장 단순한 미들웨어: 로깅
| |
이 함수는 세 단계로 커링되어 있습니다.
(store) => ...: Store에 연결될 때 한 번 호출되며,getState(),dispatch()에 접근할 수 있는store를 받는다.(next) => ...: 미들웨어 체인에서 “다음 단계"를 나타내는 함수를 받는다. 마지막 미들웨어라면next는 실제로 리듀서를 호출하는 내부 함수다.(action) => ...: 실제로 dispatch될 때마다 호출되는 부분. 여기서 로깅, API 호출 등 원하는 로직을 실행한다.
next(action)을 호출하지 않으면 무슨 일이 일어나는가
핵심 규칙은 next(action)을 호출해야만 액션이 다음 단계로 전달된다는 것입니다. 이를 직접 확인해봅시다.
| |
| |
이 예시는 실무에서 흔히 쓰이는 패턴은 아니지만, 미들웨어가 액션의 흐름 자체를 제어할 수 있다는 것을 명확히 보여줍니다. 08편에서 배운 “리듀서는 항상 액션을 받는다"는 전제는, 사실 “미들웨어가 next를 호출했을 때만” 성립하는 것입니다.
여러 미들웨어의 체인 실행
미들웨어를 여러 개 등록하면, 각 미들웨어의 next는 사실 **“다음 미들웨어의 action 처리 함수”**를 가리킵니다.
| |
sequenceDiagram
participant D as dispatch(action)
participant L as loggerMiddleware
participant B as blockNegativeMiddleware
participant R as 실제 리듀서
D->>L: action
L->>L: console.log("dispatching")
L->>B: next(action)
B->>B: 조건 검사
B->>R: next(action)
R->>R: 상태 갱신
R-->>B: 반환
B-->>L: 반환
L->>L: console.log("next state")
L-->>D: 반환
loggerMiddleware의 next가 호출하는 것은 blockNegativeMiddleware의 (action) => {...} 부분이고, blockNegativeMiddleware의 next가 호출하는 것은 실제 리듀서를 감싼 내부 dispatch 함수입니다. 등록 순서가 실행 순서를 결정하며, 앞선 미들웨어가 next(action)을 호출해야만 뒤의 미들웨어가 실행됩니다.
applyMiddleware(loggerMiddleware, blockNegativeMiddleware)가 내부적으로 하는 일은, 각 미들웨어의 next 자리에 “다음 미들웨어를 호출하는 함수"를 차례로 끼워 넣어 하나의 함수로 합성하는 것입니다. Redux는 이 합성을 compose()라는 별도의 유틸리티 함수로 구현합니다.
| |
compose(f, g, h)(x)는 f(g(h(x)))와 같습니다. 미들웨어 체인이 “첫 번째로 등록된 미들웨어가 가장 바깥에서 감싼다"는 구조를 갖는 이유가 바로 이 합성 방향 때문입니다. Redux의 미들웨어 구조는 Node.js의 Express 프레임워크가 요청을 여러 핸들러가 순서대로 처리하도록 만든 미들웨어 패턴에서 영감을 받았습니다 — “각 단계가 다음 단계로 넘길지 말지 결정한다"는 발상이 그대로 이어집니다.
타이밍 측정 미들웨어 예시
미들웨어의 실용적인 활용 예로, 액션 처리 시간을 측정하는 미들웨어를 만들어봅니다.
| |
이런 미들웨어는 리듀서 로직이 예상보다 무거워졌을 때(예: 큰 배열을 매번 정렬하는 리듀서) 개발 중에 조기 경고를 주는 용도로 쓸 수 있습니다.
Thunk·Saga·RTK Query는 모두 미들웨어다
18편에서 configureStore가 기본으로 redux-thunk 미들웨어를 포함한다고 했습니다. 22편의 Thunk, 23편의 Saga, 24편의 RTK Query는 모두 이 편에서 배운 store => next => action => {} 구조를 구현한 미들웨어입니다. 차이는 각자가 “무엇을 위해” 액션 흐름을 가로채는지에 있습니다.
- Thunk:
action이 함수일 때 그 함수를 대신 실행해준다(22편). - Saga: 제너레이터 함수로 기술된 사이드 이펙트 흐름을 실행 엔진이 관리한다(23편).
- RTK Query: 내부적으로 Thunk와 유사한 미들웨어를 통해 캐싱된 데이터 페칭을 관리한다(24편).
이 편에서 미들웨어의 기본 구조를 이해했다면, 이후 세 편은 “이 구조 위에 무엇을 얹었는가"로 훨씬 쉽게 읽힙니다.
흔한 오개념
- 미들웨어는 리듀서가 상태를 갱신한 뒤에 실행된다: 그렇지 않습니다. 미들웨어는 액션이 리듀서에 도달하기 전 단계에서 실행됩니다.
next(action)을 호출한 뒤에 실행되는 코드(loggerMiddleware의 두 번째console.log)는 “리듀서까지 포함한 나머지 체인이 모두 끝난 후"를 의미하는 것이지, 미들웨어 자체가 리듀서 뒤에 있다는 뜻이 아닙니다. next(action)을 호출하지 않으면 에러가 난다: 에러는 나지 않습니다. 단지 액션이 조용히 그 자리에서 멈출 뿐입니다.blockNegativeMiddleware예시처럼 이는 의도적으로 쓸 수도 있는 정상적인 패턴이지만, 실수로 빠뜨리면 “액션을 dispatch했는데 아무 반응이 없다"는 디버깅하기 까다로운 버그가 됩니다.
실무 체크리스트
- 커스텀 미들웨어를 작성할 때 모든 경로에서
next(action)을 호출하는지(의도적으로 막는 경우가 아니라면) 확인했는가? - 여러 미들웨어를 등록할 때 순서가 의도한 실행 흐름과 일치하는지 확인했는가?
- 리듀서 안에 넣고 싶은 유혹이 드는 부수 효과(API 호출, 로깅, 타이머)를 미들웨어로 분리하고 있는가?
연습 과제
기초(★☆☆)
loggerMiddleware를 직접 작성하고applyMiddleware로 등록해, 액션이 dispatch될 때마다 콘솔에 로그가 찍히는지 확인해보세요.
중급(★★☆)
- 특정 액션 타입(
"admin/dangerousAction")을 사용자가 관리자가 아닐 때 차단하는 미들웨어를 작성해보세요(store.getState()로 사용자 권한을 확인).
고급(★★★)
- 미들웨어 두 개를 등록 순서를 바꿔가며 실행해, 각 미들웨어의
console.log출력 순서가 어떻게 달라지는지 관찰하고 그 이유를 설명해보세요.
요약
- 미들웨어는
store => next => action => {}형태의 3중 커링 함수로, 액션이 리듀서에 도달하기 전 부수 효과를 처리하는 지점이다. next(action)을 호출해야만 액션이 다음 단계(다음 미들웨어 또는 리듀서)로 전달된다.- Thunk·Saga·RTK Query는 모두 이 미들웨어 구조 위에 구현된 도구다.
참고 문헌 및 출처(추천)
- Redux 공식 문서, “Middleware”
- Redux 공식 문서, “Writing Custom Middleware”
![Featured image of post [Redux] 21. Redux 미들웨어의 이해](/post/redux/redux-middleware/wordcloud_hu_f8cbd0b7931b7395.webp)
![[Redux] 19. createAsyncThunk - 비동기 로직 단순화](/post/redux/create-async-thunk/wordcloud_hu_37ab180eb6db5e9b.webp)
![[Redux] 20. 실습: RTK로 앱 리팩터링](/post/redux/practice-rtk-app/wordcloud_hu_97bfaa8516f2360e.webp)
![[Redux] 21. Redux 미들웨어의 이해](/post/redux/redux-middleware/wordcloud_hu_650a1d7068d6bc0d.webp)
![[Redux] 22. Redux Thunk - 가장 간단한 비동기 처리](/post/redux/redux-thunk/wordcloud_hu_b4cddaa0144503a8.webp)
![[Redux] 23. Redux Saga - 강력한 사이드 이펙트 관리](/post/redux/redux-saga/wordcloud_hu_61e6fe9d8596c675.webp)
![[Redux] 11. React-Redux 시작하기 - Provider와 connect](/post/redux/react-redux-basics/wordcloud_hu_ff09d62dce5f2caf.webp)
![[Redux] 18. configureStore - Store 설정 자동화](/post/redux/configure-store/wordcloud_hu_286e52baac5619ca.webp)
![[Tutorial] Learn Prompting - 프롬프트 엔지니어링 무료 가이드 정리](/post/2022-12-30-learn-prompting/wordcloud_hu_6a9d105de4834753.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Redux] 14. 데이터 선택자 - Selector 패턴](/post/redux/selector-patterns/wordcloud_hu_e73d33d3c19de96.webp)