본문으로 건너뛰기
비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

리듀서 로직 재사용하기

애플리케이션이 성장함에 따라 리듀서 로직에서 공통 패턴이 나타나기 시작합니다. 다양한 데이터 유형에 대해 동일한 작업을 수행하는 여러 리듀서 로직 부분을 발견하고, 각 데이터 유형에 동일한 공통 로직을 재사용하여 중복을 줄이고 싶을 수 있습니다. 또는 저장소에서 특정 유형의 데이터를 여러 "인스턴스"로 처리하고 싶을 수 있습니다. 그러나 Redux 저장소의 전역 구조에는 장단점이 있습니다: 애플리케이션의 전체 상태를 추적하기는 쉽지만, 특히 combineReducers를 사용하는 경우 특정 상태 조각을 업데이트해야 하는 액션을 "타겟팅"하기가 더 어려울 수 있습니다.

예를 들어 애플리케이션에서 A, B, C라는 이름의 여러 카운터를 추적하려 한다고 가정해 보겠습니다. 초기 counter 리듀서를 정의하고 combineReducers를 사용하여 상태를 설정합니다:

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

const rootReducer = combineReducers({
counterA: counter,
counterB: counter,
counterC: counter
})

안타깝게도 이 설정에는 문제가 있습니다. combineReducers가 각 슬라이스 리듀서에 동일한 액션을 전달하기 때문에 {type : 'INCREMENT'}를 디스패치하면 실제로 하나가 아닌 세 카운터 값 모두 증가하게 됩니다. 관심 있는 카운터만 업데이트되도록 보장할 수 있는 방법으로 counter 로직을 래핑할 필요가 있습니다.

고차 리듀서로 동작 커스터마이징하기

리듀서 로직 분할에서 정의된 바와 같이, 고차 리듀서(higher-order reducer) 는 리듀서 함수를 인수로 받거나 결과로 새 리듀서 함수를 반환하는 함수입니다. 이는 "리듀서 팩토리"로 볼 수도 있습니다. combineReducers는 고차 리듀서의 한 예입니다. 이 패턴을 사용해 특정 액션에만 반응하는 전용 버전의 리듀서 함수를 생성할 수 있습니다.

리듀서를 특수화하는 두 가지 가장 일반적인 방법은 주어진 접두사나 접미사로 새 액션 상수를 생성하거나, 액션 객체 내부에 추가 정보를 첨부하는 것입니다. 다음은 그 예시입니다:

function createCounterWithNamedType(counterName = '') {
return function counter(state = 0, action) {
switch (action.type) {
case `INCREMENT_${counterName}`:
return state + 1
case `DECREMENT_${counterName}`:
return state - 1
default:
return state
}
}
}

function createCounterWithNameData(counterName = '') {
return function counter(state = 0, action) {
const { name } = action
if (name !== counterName) return state

switch (action.type) {
case `INCREMENT`:
return state + 1
case `DECREMENT`:
return state - 1
default:
return state
}
}
}

이제 이러한 방법 중 하나를 사용해 전용 카운터 리듀서를 생성한 다음, 관심 있는 상태 부분에 영향을 주는 액션을 디스패치할 수 있어야 합니다:

const rootReducer = combineReducers({
counterA: createCounterWithNamedType('A'),
counterB: createCounterWithNamedType('B'),
counterC: createCounterWithNamedType('C')
})

store.dispatch({ type: 'INCREMENT_B' })
console.log(store.getState())
// {counterA : 0, counterB : 1, counterC : 0}

function incrementCounter(type = 'A') {
return {
type: `INCREMENT_${type}`
}
}
store.dispatch(incrementCounter('C'))
console.log(store.getState())
// {counterA : 0, counterB : 1, counterC : 1}

접근 방식을 약간 변형하여 주어진 리듀서 함수와 이름/식별자를 모두 받는 더 일반적인 고차 리듀서를 생성할 수도 있습니다:

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

function createNamedWrapperReducer(reducerFunction, reducerName) {
return (state, action) => {
const { name } = action
const isInitializationCall = state === undefined
if (name !== reducerName && !isInitializationCall) return state

return reducerFunction(state, action)
}
}

const rootReducer = combineReducers({
counterA: createNamedWrapperReducer(counter, 'A'),
counterB: createNamedWrapperReducer(counter, 'B'),
counterC: createNamedWrapperReducer(counter, 'C')
})

필터링 기능을 하는 일반적인 고차 리듀서를 만들 수도 있습니다:

function createFilteredReducer(reducerFunction, reducerPredicate) {
return (state, action) => {
const isInitializationCall = state === undefined;
const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall;
return shouldRunWrappedReducer ? reducerFunction(state, action) : state;
}
}

const rootReducer = combineReducers({
// check for suffixed strings
counterA : createFilteredReducer(counter, action => action.type.endsWith('_A')),
// check for extra data in the action
counterB : createFilteredReducer(counter, action => action.name === 'B'),
// respond to all 'INCREMENT' actions, but never 'DECREMENT'
counterC : createFilteredReducer(counter, action => action.type === 'INCREMENT')
};

이러한 기본 패턴을 사용하면 UI 내에서 스마트 연결 컴포넌트의 여러 인스턴스를 갖거나, 페이징이나 정렬과 같은 일반적인 기능에 공통 로직을 재사용할 수 있습니다.

이런 방식으로 리듀서를 생성하는 것 외에도 동일한 접근 방식으로 액션 생성자를 생성할 수 있으며, 헬퍼 함수를 사용해 둘을 동시에 생성할 수도 있습니다. 액션/리듀서 유틸리티는 Action/Reducer GeneratorsReducers 라이브러리를 참조하세요.

컬렉션/항목 리듀서 패턴

이 패턴을 사용하면 여러 상태를 보유하고 액션 객체 내부의 추가 매개변수를 기반으로 각 상태를 업데이트하는 공통 리듀서를 사용할 수 있습니다.

function counterReducer(state, action) {
switch(action.type) {
case "INCREMENT" : return state + 1;
case "DECREMENT" : return state - 1;
}
}

function countersArrayReducer(state, action) {
switch(action.type) {
case "INCREMENT":
case "DECREMENT":
return state.map( (counter, index) => {
if(index !== action.index) return counter;
return counterReducer(counter, action);
});
default:
return state;
}
}

function countersMapReducer(state, action) {
switch(action.type) {
case "INCREMENT":
case "DECREMENT":
return {
...state,
state[action.name] : counterReducer(state[action.name], action)
};
default:
return state;
}
}