이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
combineReducers의 한계를 넘어서
Redux에 포함된 combineReducers 유틸리티는 매우 유용하지만, 의도적으로 단일 공통 사용 사례에만 제한되어 있습니다: 일반 자바스크립트 객체인 상태 트리를 업데이트할 때, 각 상태 조각의 업데이트 작업을 특정 슬라이스 리듀서에 위임하는 것입니다. 이 유틸리티는 Immutable.js Maps로 구성된 상태 트리, 상태 트리의 다른 부분을 슬라이스 리듀서에 추가 인수로 전달하려는 시도, 슬라이스 리듀서 호출의 "순서 지정"과 같은 다른 사용 사례를 처리하지 않습니다. 또한 특정 슬라이스 리듀서가 작업을 수행하는 방식에는 관여하지 않습니다.
그렇다면 흔히 묻는 질문은 "다른 사용 사례를 처리하기 위해 combineReducers를 어떻게 사용할 수 있나요?"입니다. 이에 대한 답은 간단합니다: "사용하지 않습니다. 다른 방법이 필요할 것입니다". combineReducers의 핵심 사용 사례를 벗어나면 더 '커스텀'한 리듀서 로직을 사용해야 할 때입니다. 이는 일회성 사용 사례를 위한 특정 로직이거나 널리 공유될 수 있는 재사용 가능한 함수일 수 있습니다. 여기서는 몇 가지 전형적인 사용 사례를 처리하기 위한 제안을 드리지만, 자유롭게 여러분만의 접근 방식을 고안해 보세요.
슬라이스 리듀서 간 데이터 공유
마찬가지로, 특정 액션을 처리하기 위해 sliceReducerA가 sliceReducerB의 상태 조각에서 일부 데이터가 필요하거나, sliceReducerB가 전체 상태를 인수로 필요로 하는 경우, combineReducers 자체는 이를 처리하지 못합니다. 이는 다음과 같이 필요한 데이터를 특정 사례에서 추가 인수로 전달하는 방법을 아는 커스텀 함수를 작성하여 해결할 수 있습니다:
function combinedReducer(state, action) {
switch (action.type) {
case 'A_TYPICAL_ACTION': {
return {
a: sliceReducerA(state.a, action),
b: sliceReducerB(state.b, action)
}
}
case 'SOME_SPECIAL_ACTION': {
return {
// specifically pass state.b as an additional argument
a: sliceReducerA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
case 'ANOTHER_SPECIAL_ACTION': {
return {
a: sliceReducerA(state.a, action),
// specifically pass the entire state as an additional argument
b: sliceReducerB(state.b, action, state)
}
}
default:
return state
}
}
"슬라이스 간 업데이트 공유" 문제에 대한 또 다른 대안은 단순히 액션에 더 많은 데이터를 포함시키는 것입니다. 이는 다음 예제와 같이 썽크 함수(thunk functions)나 유사한 접근 방식을 사용해 쉽게 달성할 수 있습니다:
function someSpecialActionCreator() {
return (dispatch, getState) => {
const state = getState()
const dataFromB = selectImportantDataFromB(state)
dispatch({
type: 'SOME_SPECIAL_ACTION',
payload: {
dataFromB
}
})
}
}
B의 슬라이스 데이터가 이미 액션에 포함되어 있으므로, 부모 리듀서는 sliceReducerA가 해당 데이터를 사용할 수 있도록 특별한 작업을 할 필요가 없습니다.
세 번째 접근 방식은 combineReducers로 생성된 리듀서를 사용해 각 슬라이스 리듀서가 독립적으로 업데이트할 수 있는 "단순한" 경우를 처리하고, 슬라이스 간 데이터 공유가 필요한 "특별한" 경우를 처리하기 위해 다른 리듀서를 추가로 사용하는 것입니다. 그런 다음 래핑 함수가 이 두 리듀서를 차례로 호출해 최종 결과를 생성합니다:
const combinedReducer = combineReducers({
a: sliceReducerA,
b: sliceReducerB
})
function crossSliceReducer(state, action) {
switch (action.type) {
case 'SOME_SPECIAL_ACTION': {
return {
// specifically pass state.b as an additional argument
a: handleSpecialCaseForA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
default:
return state
}
}
function rootReducer(state, action) {
const intermediateState = combinedReducer(state, action)
const finalState = crossSliceReducer(intermediateState, action)
return finalState
}
사실 reduce-reducers라는 유용한 유틸리티가 있어 이 과정을 더 쉽게 만들 수 있습니다. 이 유틸리티는 여러 리듀서를 받아 reduce()를 실행하고, 중간 상태 값을 다음 리듀서로 전달합니다:
// Same as the "manual" rootReducer above
const rootReducer = reduceReducers(combinedReducers, crossSliceReducer)
reduceReducers를 사용할 때는 목록의 첫 번째 리듀서가 초기 상태를 정의할 수 있도록 해야 합니다. 이후 리듀서들은 일반적으로 전체 상태가 이미 존재한다고 가정하고 기본값 제공을 시도하지 않기 때문입니다.
추가 제안
다시 강조하지만, Redux 리듀서는 단순히 함수라는 점을 이해하는 것이 중요합니다. combineReducers가 유용하지만, 이는 도구 상자 속 하나의 도구일 뿐입니다. 함수는 switch 문 외에 다른 조건부 로직을 포함할 수 있으며, 함수는 서로를 래핑하기 위해 구성될 수 있고, 함수는 다른 함수를 호출할 수 있습니다. 예를 들어 특정 슬라이스 리듀서가 상태를 재설정할 수 있도록 하고 전체적으로 특정 액션에만 응답하도록 해야 할 수도 있습니다. 다음과 같이 할 수 있습니다:
const undoableFilteredSliceA = compose(
undoReducer,
filterReducer('ACTION_1', 'ACTION_2'),
sliceReducerA
)
const rootReducer = combineReducers({
a: undoableFilteredSliceA,
b: normalSliceReducerB
})
combineReducers는 a 관리를 담당하는 리듀서 함수에 특별한 점이 있다는 것을 알지 못하며 관심도 없습니다. 실행 취소 방법을 알기 위해 combineReducers를 수정할 필요가 없었습니다. 필요한 조각들을 모아 새로운 구성 함수로 만들었을 뿐입니다.
또한 combineReducers가 Redux에 내장된 유일한 리듀서 유틸리티 함수이지만, 재사용 가능하도록 공개된 다양한 서드파티 리듀서 유틸리티들이 존재합니다. Redux 애드온 카탈로그에서 사용 가능한 여러 서드파티 유틸리티 목록을 확인할 수 있습니다. 또는 공개된 유틸리티 중 사용 사례를 해결할 수 있는 것이 없다면, 정확히 필요한 작업을 수행하는 함수를 직접 작성할 수도 있습니다.