이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
커스텀 미들웨어 작성하기
- 커스텀 미들웨어를 사용하는 경우
- 미들웨어의 표준 패턴
- 다른 Redux 프로젝트와의 호환성 보장 방법
Redux에서 미들웨어는 주로 다음 용도로 사용할 수 있습니다:
-
액션에 대한 사이드 이펙트 생성
-
액션 수정 또는 취소
-
디스패치에서 허용하는 입력 수정
대부분의 사용 사례는 첫 번째 범주에 속합니다: 예를 들어 Redux-Saga, redux-observable, RTK listener middleware는 모두 액션에 반응하는 사이드 이펙트를 생성합니다. 이러한 예제들은 상태 변경 외에 액션에 반응할 수 있어야 한다는 매우 일반적인 필요성을 보여줍니다.
액션 수정은 상태나 외부 입력의 정보로 액션을 강화하거나, 스로틀링(throttle), 디바운싱(debounce), 게이팅(gate)하는 데 사용될 수 있습니다.
디스패치 입력 수정의 가장 대표적인 예는 Redux Thunk로, 액션을 반환하는 함수를 호출하여 액션으로 변환합니다.
커스텀 미들웨어 사용 시기
대부분의 경우 실제로 커스텀 미들웨어가 필요하지 않습니다. 가장 흔한 미들웨어 사용 사례는 사이드 이펙트이며, Redux용 사이드 이펙트를 잘 패키징한 수많은 패키지들이 존재하며 오랫동안 사용되며 직접 구현 시 발생할 수 있는 미묘한 문제를 해결했습니다. 서버 사이드 상태 관리는 RTK Query를, 다른 사이드 이펙트는 RTK listener middleware를 시작점으로 삼는 것이 좋습니다.
다음 두 가지 경우에는 여전히 커스텀 미들웨어를 사용할 수 있습니다:
-
단일하고 매우 간단한 사이드 이펙트만 있는 경우: 완전한 추가 프레임워크 도입이 가치가 없을 수 있습니다. 애플리케이션이 성장하면 커스텀 솔루션을 확장하기보다 기존 프레임워크로 전환해야 합니다.
-
액션을 수정하거나 취소해야 하는 경우
미들웨어 표준 패턴
액션에 대한 사이드 이펙트 생성
가장 일반적인 미들웨어 유형입니다. rtk listener middleware의 구조를 살펴보세요:
const middleware: ListenerMiddleware<S, D, ExtraArgument> =
api => next => action => {
if (addListener.match(action)) {
return startListening(action.payload)
}
if (clearAllListeners.match(action)) {
clearListenerMiddleware()
return
}
if (removeListener.match(action)) {
return stopListening(action.payload)
}
// Need to get this state _before_ the reducer processes the action
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()
// `getOriginalState` can only be called synchronously.
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
const getOriginalState = (): S => {
if (originalState === INTERNAL_NIL_TOKEN) {
throw new Error(
`${alm}: getOriginalState can only be called synchronously`
)
}
return originalState as S
}
let result: unknown
try {
// Actually forward the action to the reducer before we handle listeners
result = next(action)
if (listenerMap.size > 0) {
let currentState = api.getState()
// Work around ESBuild+TS transpilation issue
const listenerEntries = Array.from(listenerMap.values())
for (let entry of listenerEntries) {
let runListener = false
try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
runListener = false
safelyNotifyError(onError, predicateError, {
raisedBy: 'predicate'
})
}
if (!runListener) {
continue
}
notifyListener(entry, action, api, getOriginalState)
}
}
} finally {
// Remove `originalState` store from this scope.
originalState = INTERNAL_NIL_TOKEN
}
return result
}
첫 번째 부분에서는 addListener, clearAllListeners, removeListener 액션을 수신하여 이후 호출할 리스너를 변경합니다.
두 번째 부분에서는 주로 액션이 다른 미들웨어와 리듀서를 통과한 후의 상태를 계산한 다음, 원본 상태와 리듀서에서 생성된 새 상태를 모두 리스너에게 전달합니다.
액션 디스패치 후 사이드 이펙트를 발생시키는 것이 일반적인데, 이는 원본 상태와 새 상태를 모두 고려할 수 있게 하며, 사이드 이펙트에서 비롯된 상호작용이 현재 액션 실행에 영향을 미쳐서는 안 되기 때문입니다(그렇지 않다면 사이드 이펙트라고 할 수 없습니다).
액션 수정/취소 또는 디스패치 입력 수정
이러한 패턴들은 덜 일반적이지만, 대부분(액션 취소 제외)은 redux thunk middleware에서 사용됩니다:
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}
// Otherwise, pass the action down the middleware chain as usual
return next(action)
}
일반적으로 dispatch는 JSON 형태의 액션만 처리할 수 있습니다. 이 미들웨어는 함수 형태의 액션도 처리할 수 있는 기능을 추가하며, 함수-액션의 반환 값을 dispatch 함수의 반환 값으로 전달함으로써 dispatch 함수 자체의 반환 타입을 변경합니다.
호환 가능한 미들웨어 작성 규칙
원칙적으로 미들웨어는 매우 강력한 패턴이며 액션에 대해 원하는 모든 작업을 수행할 수 있습니다. 그러나 기존 미들웨어는 주변 미들웨어에서 발생하는 상황에 대한 가정을 가지고 있을 수 있으며, 이러한 가정을 인지하면 기존에 널리 사용되는 미들웨어와의 호환성을 보장하기가 더 쉬워집니다.
우리 미들웨어와 다른 미들웨어 사이에는 두 가지 접점이 있습니다:
다음 미들웨어 호출하기
next를 호출할 때 미들웨어는 어떤 형태의 액션을 기대합니다. 명시적으로 수정하려는 의도가 없다면 받은 액션을 그대로 전달해야 합니다.
더 미묘한 점은, 일부 미들웨어는 dispatch가 호출된 동일한 실행 주기에서 미들웨어가 호출되기를 기대하므로 next를 동기적으로 호출해야 합니다.
dispatch 반환 값 처리
미들웨어가 dispatch의 반환 값을 명시적으로 수정할 필요가 없다면 next에서 받은 값을 그대로 반환하세요. 반환 값을 수정해야 한다면, 미들웨어 체인에서 매우 특정한 위치에 미들웨어를 배치해야 의도한 작업을 수행할 수 있습니다. 이 경우 다른 모든 미들웨어와의 호환성을 수동으로 확인하고 상호작용 방식을 결정해야 합니다.
여기에는 까다로운 결과가 따릅니다:
const middleware: Middleware = api => next => async action => {
const response = next(action)
// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}
return response
}
응답을 수정하지 않은 것처럼 보이지만 실제로는 수정한 것입니다: async-await로 인해 이제 반환 값이 프로미스가 되었기 때문입니다. 이는 RTK Query와 같은 일부 미들웨어를 손상시킬 수 있습니다.
그렇다면 이 미들웨어를 어떻게 작성해야 할까요?
const middleware: Middleware = api => next => action => {
const response = next(action)
// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
void loadData(api)
}
return response
}
async function loadData(api) {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}
비동기 로직을 별도의 함수로 분리하여 async-await를 계속 사용하되, 미들웨어 내에서 프로미스 해결을 실제로 기다리지 마세요. void는 코드 동작에 영향을 주지 않으면서 프로미스를 명시적으로 기다리지 않기로 결정했음을 다른 개발자에게 나타냅니다.
다음 단계
아직 확인하지 않으셨다면, 미들웨어의 내부 작동 방식을 이해하기 위해 Redux 이해하기의 미들웨어 섹션을 살펴보세요.