이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
함수 분해와 리듀서 조합을 통한 리듀서 로직 리팩토링
다양한 유형의 서브 리듀서 함수들이 어떻게 생겼으며 어떻게 조합되는지 예시를 보는 것이 도움이 될 수 있습니다. 하나의 거대한 리듀서 함수를 여러 개의 작은 함수들로 구성된 형태로 어떻게 리팩토링하는지 살펴보겠습니다.
참고: 이 예제는 개념 설명과 리팩토링 과정을 명확히 보여주기 위해 의도적으로 장황하게 작성되었으며, 간결함보다는 이해를 우선시했습니다.
초기 리듀서
초기 리듀서가 다음과 같다고 가정해 봅시다:
const initialState = {
visibilityFilter: 'SHOW_ALL',
todos: []
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return Object.assign({}, state, {
visibilityFilter: action.filter
})
}
case 'ADD_TODO': {
return Object.assign({}, state, {
todos: state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
case 'TOGGLE_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
completed: !todo.completed
})
})
})
}
case 'EDIT_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
text: action.text
})
})
})
}
default:
return state
}
}
이 함수는 상대적으로 짧지만 이미 지나치게 복잡해지고 있습니다. 두 가지 다른 관심사(필터링 vs 할 일 목록 관리)를 다루고 있으며, 중첩 구조로 인해 업데이트 로직을 읽기 어렵고 모든 부분에서 무슨 일이 일어나는지 명확하지 않습니다.
유틸리티 함수 추출
첫 번째 단계로 업데이트된 필드가 포함된 새 객체를 반환하는 유틸리티 함수를 분리하는 것이 좋습니다. 또한 배열 내 특정 항목을 업데이트하려는 반복되는 패턴을 함수로 추출할 수 있습니다:
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return updateObject(state, { visibilityFilter: action.filter })
}
case 'ADD_TODO': {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
case 'TOGGLE_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
case 'EDIT_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
default:
return state
}
}
이렇게 하면 중복이 줄어들고 가독성이 조금 향상됩니다.
케이스 리듀서 추출
다음으로 각 특정 케이스를 별도의 함수로 분리할 수 있습니다:
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(state, action) {
return updateObject(state, { visibilityFilter: action.filter })
}
function addTodo(state, action) {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
function toggleTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
function editTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(state, action)
case 'ADD_TODO':
return addTodo(state, action)
case 'TOGGLE_TODO':
return toggleTodo(state, action)
case 'EDIT_TODO':
return editTodo(state, action)
default:
return state
}
}
이제 각 케이스에서 무슨 일이 발생하는지 매우 명확해졌습니다. 또한 몇 가지 패턴이 나타나기 시작하는 것도 볼 수 있습니다.
도메인별 데이터 처리 분리
우리의 앱 리듀서는 여전히 모든 애플리케이션 케이스를 알고 있습니다. 필터 로직과 할 일 로직을 분리해 보겠습니다:
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}
function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(visibilityState, action)
default:
return visibilityState
}
}
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
function todosReducer(todosState = [], action) {
switch (action.type) {
case 'ADD_TODO':
return addTodo(todosState, action)
case 'TOGGLE_TODO':
return toggleTodo(todosState, action)
case 'EDIT_TODO':
return editTodo(todosState, action)
default:
return todosState
}
}
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
두 "상태 조각" 리듀서가 이제 전체 상태 중 자신의 부분만 인자로 받게 되면서, 복잡한 중첩 상태 객체를 반환할 필요가 없어지고 결과적으로 더 단순해졌습니다.
보일러플레이트 줄이기
거의 다 왔습니다. 많은 사람들이 switch 문을 선호하지 않기 때문에, 액션 유형과 케이스 함수를 매핑하는 룩업 테이블을 생성하는 함수를 사용하는 것이 일반적입니다. 보일러플레이트 줄이기에서 설명한 createReducer 함수를 사용하겠습니다:
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// Omitted
function setVisibilityFilter(visibilityState, action) {}
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// Omitted
function addTodo(todosState, action) {}
function toggleTodo(todosState, action) {}
function editTodo(todosState, action) {}
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
조각별 리듀서 결합
마지막 단계로 Redux의 내장 combineReducers 유틸리티를 사용하여 최상위 앱 리듀서의 "상태 조각" 로직을 처리할 수 있습니다. 최종 결과는 다음과 같습니다:
// Reusable utility functions
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// Handler for a specific case ("case reducer")
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}
// Handler for an entire slice of state ("slice reducer")
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// Case reducer
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
// Case reducer
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
// Case reducer
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
// Slice reducer
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
// "Root reducer"
const appReducer = combineReducers({
visibilityFilter: visibilityReducer,
todos: todosReducer
})
이제 여러 종류의 분할된 리듀서 함수 예시들을 확인할 수 있습니다: updateObject 및 createReducer 같은 도우미 유틸리티, setVisibilityFilter 및 addTodo 같은 특정 케이스 핸들러, visibilityReducer 및 todosReducer 같은 상태 조각 핸들러 등입니다. 또한 appReducer가 "루트 리듀서"의 예시임을 알 수 있습니다.
이 예제의 최종 결과가 원본 버전보다 눈에 띄게 길어졌지만, 이는 주로 유틸리티 함수 추출, 주석 추가, 명확성을 위한 의도적인 장문화(예: 별도의 return 문 사용) 때문입니다. 각 함수를 개별적으로 보면 책임 범위가 더 작아졌고 의도가 더 명확해졌습니다. 또한 실제 애플리케이션에서는 이러한 함수들을 reducerUtilities.js, visibilityReducer.js, todosReducer.js, rootReducer.js 같은 별도 파일로 분리할 것입니다.