3-3-리듀서-리팩토링하기

  1. 유틸리티 함수 추출

  2. 케이스 리듀서 추출

  3. 데이터 관리를 도메인으로 분리하기

  4. 보일러 플레이트 줄이기

  5. 슬라이스로 리듀서 결합하기

초기 리듀서

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;
    }
}

유틸리티 함수 추출하기

위 리듀서에서 배열의 특정 항목을 업데이트하는 패턴이 발견할 수 있다. 업데이트된 필드를 포함하여 새로운 객체를 반환하는 함수를 만들어보자.

function updateObject(oldObject, newValues) {
  // 새로운 객체를 첫번째 매개변수로 전달한다는 아이디어를 캡슐화
  // 데이터를 변경하는 대신에 데이터를 확실히 복사하기 위해 Object.assign을 사용합니다.
  return Object.assign({}, oldObject, newValues);
}

function updateItemInArray(array, itemId, updateItemCallback) {
  const updatedItems = array.map(item => {
    if(item.id !== itemId) {
      // 한가지 항목만 업데이트하기 때문에 다른 항목은 유지합니다.
      return item;
    }

    // 업데이트 된 항목을 만들기 위해 주어진 콜백을 사용합니다.
    const updatedItem = updateItemCallback(item);
    return updatedItem;
  });

  return updatedItems;
}

updateItemInArray의 updateItemCallback으로 업데이트 방식 정하는거 좋은듯

케이스 리듀서 추출

액션 별로 리듀서 동작을 함수로 정의하여 호출. 무슨일 일어나는지 명확해지고, 어떤 패턴이 보이기 시작할거다?

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;
    }
}

케이스 리듀서 추출

각 케이스를 자체함수로 나눌 수 있다.

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;
    }
}

각 액션에 대해 무슨 일이 일어나는지 매우 명확하다. 또한, 어떤 패턴이 나타나기 시작함을 볼 수 있다.

데이터 관리 도메인으로 분리하기

필터 로직과 todo로직을 분리시키자.

function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
    switch(action.type) {
        case 'SET_VISIBILITY_FILTER' : return setVisibilityFilter(visibilityState, action);
        default : return visibilityState;
    }
};

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문을 케이스 함수를 위한 ActionType의 룩업테이블 만들기. createReducer를 통해 보일러 플레이트를 줄여보자.

function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action)
    } else {
      return state
    }
  }
}

const visibilityReducer = createReducer('SHOW_ALL', {
    'SET_VISIBILITY_FILTER' : setVisibilityFilter
});

const todosReducer = createReducer([], {
    'ADD_TODO' : addTodo,
    'TOGGLE_TODO' : toggleTodo,
    'EDIT_TODO' : editTodo
});

슬라이스로 리듀서 결합하기

최상위 앱 리듀서에 대한 "상태의 조각"의 로직을 처리하기 위해 combineReducers 유틸을 사용할 수 있다.

// "루트 리듀서"
const appReducer = combineReducers({
    visibilityFilter : visibilityReducer,
    todos : todosReducer
});

앞에서 설명한 리듀서 종류를 참고하여 전체코드를 다시 보자

// 재사용 가능한 유틸리티 함수

function updateObject(oldObject, newValues) {
    // 새로운 객체를 첫번째 매개변수로 전달한다는 아이디어를 캡슐화
    // 데이터를 변경하는 대신에 데이터를 확실히 복사하기 위해 Object.assign을 사용합니다.
    return Object.assign({}, oldObject, newValues);
}

function updateItemInArray(array, itemId, updateItemCallback) {
    const updatedItems = array.map(item => {
        if(item.id !== itemId) {
            // 한가지 항목만 업데이트하기 때문에 다른 항목은 유지합니다.
            return 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
    }
  }
}


// 특정 케이스의 핸들러 ("케이스 리듀서")
function setVisibilityFilter(visibilityState, action) {
    // 기술적으로, 우리는 이전의 상태에 대해서도 신경쓰지 않습니다.
    return action.filter;
}

// 전체 상태의 조각에 대한 핸들러("슬라이스 리듀서")
const visibilityReducer = createReducer('SHOW_ALL', {
    'SET_VISIBILITY_FILTER' : setVisibilityFilter
});

// 케이스 리듀서
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;
}

// 슬라이스 리듀서
const todosReducer = createReducer([], {
    'ADD_TODO' : addTodo,
    'TOGGLE_TODO' : toggleTodo,
    'EDIT_TODO' : editTodo
});

// "루트 리듀서"
const appReducer = combineReducers({
    visibilityFilter : visibilityReducer,
    todos : todosReducer
});
  • helperUtility

    • updateObject

    • createReducer

  • 케이스 리듀서: case에 대한 handler

    • setVisibilityFilter

    • addTodo

  • 슬라이스 리듀서: 전체 상태의 조각에 대한 핸들러

    • visibilityReducer

    • todosReducer

  • 루트 리듀서

    • appReducer

위와 같은 리팩토링을 통해, 각 함수의 책임이 줄었고 의도가 명확해졌다. 실제로 작성하게되면 reducerUtilities.js, visibilityReducer.js, todosReducer.js, rootReducer.js와 같이 파일로 분리될거다.

Last updated

Was this helpful?