쏙쏙 들어오는 함수형 코딩 CH4

Yunes·2023년 9월 29일
0
post-thumbnail

바쁜 현대인을 위한 요약

Ch4 에서는 액션으로부터 계산을 어떻게 빼낼 수 있는지에 대해 소개하고 있다.

Ch4 요약

일반적으로 액션은 암묵적 입력과 출력을 갖는다. 이때 암묵적 입력과 출력은 인자와 return 을 제외한 나머지 입, 출력을 의미한다.

액션과 비즈니스 규칙을 분리하자.

계산은 암묵적 입력과 출력이 없어야 한다.

암묵적 입력은 인자로, 암묵적 출력은 리턴값으로 바꿀 수 있다.

액션에서 계산을 빼내는 작업은 1. 계산 코드를 찾아 빼낸다. 2.새 함수에 암묵적 입력과 출력을 찾는다. 3. 암묵적 입력은 인자로 암묵적 출력은 리턴값으로 바꾼다. 의 과정을 통해 진행한다.

CH4 에서 두가지 리팩토링 기법을 소개한다.

  1. 서브루틴 추출하기
  2. 함수 추출하기

서브루틴 추출하기

/// Original
function calc_cart_total() {
  shopping_cart_total = 0;
  for(var i = 0; i < shopping_cart.length; i++) {
    var item = shopping_cart[i];
    shopping_cart_total += item.price;
  }

  set_cart_total_dom();
  update_shipping_icons();
  update_tax_dom();
}

/// 암묵적 입,출력 제거
function calc_cart_total() {
  shopping_cart_total = calc_total(shopping_cart);
  set_cart_total_dom();
  update_shipping_icons();
  update_tax_dom();
}

function calc_total(cart) {
  var total = 0;
  for(var i = 0; i < cart.length; i++) {
    var item = cart[i];
    total += item.price;
  }
  return total;
}

함수 추출하기

/// Original
function add_item_to_cart(name, price) {
  shopping_cart.push({
    name: name,
    price: price
  });

  calc_cart_total();
}

/// 암묵적 입, 출력 제거
function add_item_to_cart(name, price) {
  shopping_cart = add_item(shopping_cart, name, price);
  calc_cart_total();
}

function add_item(cart, name, price) {
  var new_cart = cart.slice();
  new_cart.push({
    name: name,
    price: price
  });
  return new_cart;
}

TMI 인데 이 리펙토링 기법을 보다가 꼭 리펙토링 2nd 책으로 북스터디를 진행해야겠다는 생각이 들었다.

책에 예시코드가 자세히 나와있어서 참 좋다. 액션으로부터 어떻게 계산을 추출했는지 이해하기 쉽다.

실제로 적용해보기

다음은 redux 를 이용해 todo 앱을 만들어보았을 때 만들었던 todosSlice.js 코드이다.

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  todos: [
    { id: 0, text: "Learn React", completed: true },
    { id: 1, text: "Learn Redux", completed: false },
  ],
};

// Create a utility function to generate the next todo ID
function nextTodoId(todos) {
  const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1);
  return maxId + 1;
}

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    todoAdded: (state, action) => {
      state.todos.push({
        id: nextTodoId(state.todos),
        text: action.payload,
        completed: false,
      });
    },
    todoToggled: (state, action) => {
      const toggledTodo = state.todos.find(
        (todo) => todo.id === action.payload
      );
      if (toggledTodo) {
        toggledTodo.completed = !toggledTodo.completed;
      }
    },
    todoDeleted: (state, action) => {
      state.todos = state.todos.filter((todo) => todo.id !== action.payload);
    },
    allCompleted: (state) => {
      state.todos = state.todos.map((todo) => ({
        ...todo,
        completed: true,
      }));
    },
    completedCleared: (state) => {
      state.todos = state.todos.filter((todo) => !todo.completed);
    },
  },
});

export const maxId = (todos) =>
  todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1;

export const todos = (state) => state.todos;

export const completedTodos = (state) =>
  state.todos.filter((todo) => todo.completed === true);

export const {
  todoAdded,
  todoToggled,
  todoDeleted,
  allCompleted,
  completedCleared,
} = todosSlice.actions;

export default todosSlice.reducer;

여기서 리듀서를 보니 함수인데 반환값이 없는 경우도 있고 전역 변수는 아니지만 비즈니스 로직을 분리할 수 있을 것 같아서 CH4 의 액션으로부터 계산 빼내기를 적용해보았다.

allCompleted 리펙토링

// 기존코드
allCompleted: (state) => {
  state.todos = state.todos.map((todo) => ({
    ...todo,
    completed: true,
  }));
},
      
// 리펙토링한 코드
allCompleted: (state) => {
  state.todos = todo_all_completed(state.todos);
},

// 함수 추출하기 
function todo_all_completed(todos) { // 함수에 존재하던 암묵적 입력을 인자로 대체
  const newTodos = todos.slice(); // 원본을 건드리지 않고 얕은 복사로 새 배열을 만들어줬다.
  return newTodos.map((todo) => ({  // 명시적 출력을 생성
    ...todo,
    completed: true,
  }));
}

유사한 방법으로 각 리듀서들을 리펙토링해봤다.

completedCleared 리펙토링

// 원본
completedCleared: (state) => {
  state.todos = state.todos.filter((todo) => !todo.completed);
},
  
// 리펙토링한 코드
completedCleared: (state) => {
  state.todos = todo_completed_cleared(state.todos);
},
  
function todo_completed_cleared(todos) {
  const newTodos = todos.slice();
  return newTodos.filter((todo) => !todo.completed);
}

todoDeleted 리펙토링

// 원본
todoDeleted: (state, action) => {
  state.todos = state.todos.filter((todo) => todo.id !== action.payload);
},
  
// 리펙토링
todoDeleted: (state, action) => {
  state.todos = todo_deleted(state.todos, action.payload);
},
  
function todo_deleted(todos, id) {
  const newTodos = todos.slice();
  return newTodos.filter((todo) => todo.id !== id);
}

todoToggled 리펙토링

사실 이 코드가 당연히 동작하겠지 싶어서 작성했다가 지금 리펙토링할때 새로 알게된 사실이 있다.

JavaScript 에서 Array.find(); 메서드의 반환값을 수정하면 원본도 수정된다는 것을 새로 알게 되었다.

// 원본
todoToggled: (state, action) => {
  const toggledTodo = state.todos.find(
    (todo) => todo.id === action.payload
  );
  if (toggledTodo) {
    toggledTodo.completed = !toggledTodo.completed;
  }
},

위의 코드를 보고 아.. state.todos 원본을 수정하지 않았네 코드 수정해야겠다.. 라고 생각하고 코드를 아래와 같이 수정하려고 했다.

todoToggled: (state, action) => {
  state.todos = state.todos.map((todo) => {
    return todo.id === action.payload
      ? (todo.completed = !todo.completed)
    : todo;
  });
},

그런데 동작하는 모습이 같아서 mdn 을 찾아봤다.

const inventory = [
  { name: "apples", quantity: 2 },
  { name: "bananas", quantity: 0 },
  { name: "cherries", quantity: 5 },
];

function isCherries(fruit) {
  return fruit.name === "cherries";
}

console.log(inventory.find(isCherries));
// { name: 'cherries', quantity: 5 }

하단의 map, find, filter 가 얕은 복사를 한다고 생각했던 부분은 오해였다. 구체적으로는 Ch7 파트에 정리해두었다.

나는 find 가 배열에서 조건을 만족하는 해당 인덱스의 모든 값을 반환해주는 메서드라고 생각했는데 그 반환값을 직접 수정하면 원본도 수정이 된다.

filter 에 대해서도 같은 코드를 실행해봤는데 filter 의 반환값을 수정하면 원본도 수정된다.

???

이게뭐지 filter 도 반환값을 수정하니 원본도 같이 수정된다.

map 도 마찬가지로 map 의 반환값이 갖는 데이터가 원본과 달라졌으나 map 의 반환값을 수정하니 원본의 값도 같이 수정되었다.

갑자기 딴길로 샌 느낌이 있지만 find, filter, map 의 반환값이 원본과 같은 주소를 가리키고 있음을 알 수 있었다.

다시 리펙토링을 진행한 결과

// 원본
todoToggled: (state, action) => {
  const toggledTodo = state.todos.find(
    (todo) => todo.id === action.payload
  );
  if (toggledTodo) {
    toggledTodo.completed = !toggledTodo.completed;
  }
},
  
// 리펙토링
todoToggled: (state, action) => {
  state.todos = todo_toggled(state.todos, action.payload);
},
  
function todo_toggled(todos, id) {
  const newTodos = todos.slice();
  const newToggledTodo = newTodos.find((todo) => todo.id === id);
  if (newToggledTodo) newToggledTodo.completed = !newToggledTodo.completed;

  return newTodos;
}

todoAdded 리펙토링

// 원본
todoAdded: (state, action) => {
  state.todos.push({
    id: nextTodoId(state.todos),
    text: action.payload,
    completed: false,
  });
},

// 리펙토링
todoAdded: (state, action) => {
  state.todos = todo_added(state.todos, action.payload);
},

function todo_added(todos, text) {
  const newTodos = todos.slice();
  newTodos.push({
    id: nextTodoId(todos),
    text,
    completed: false,
  });
  return newTodos;
}

function nextTodoId(todos) {
  const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1);
  return maxId + 1;
}

확실히 액션에서 계산만 뽑아내니 테스트코드는 순수함수인 계산에 대해서만 작성하기 용이해졌다.

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글