조컴퓨터

챕터 4. 액션에서 계산 빼내기(리팩터링) 본문

책읽기/쏙쏙 들어오는 함수형 코딩

챕터 4. 액션에서 계산 빼내기(리팩터링)

챠오위 2022. 8. 8. 01:14

챕터 4에서 중점적으로 볼 내용은 ? 

- 어떻게 함수로 정보가 들어가고 나오는지

- 테스트하기 쉽고 재사용성이 좋은 코드를 만들기 위한 함수형 기술에 대해 알아보기

- 액션에서 계산을 빼내는 방법 배우기

 

 

 

MegaMart.com 에 오신 것을 환영합니다

var shopping_cart = [];   // 장바구니 제품과 금액 합계를 담고 있는 전역변수

var shopping_cart_total = 0;

 

function add_item_to_cart(name, price) {

   shopping_cart.push({   // 장바구니에 제품을 담기 위해 cart 배열에 레코드를 추가

      name : name,

      price : price

   });

   calc_cart_total();   // 금액 합계 업데이트 

}

 

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();   // DOM 업데이트

}

 

 

무료 배송비 계산하기

새로운 요구사항

구매 합계가 20 달러 이상이면 무료 배송을 한다. 20 달러가 넘는 제품의 구매 버튼 옆에 무료 배송 아이콘을 표시해 주려고 한다.

 

절차적인 방법으로 구현하기

function update_shipping_icons() {

   var buy_buttons = get_buy_buttons_dom();   // 페이지에 있는 모든 구매 버튼을 가져와 반복문 적용

   for (var i = 0; i < buy_buttons.length; i++) {

      var button = buy_buttons[i];

      var item = button.item;

      if (item.price + shopping_cart_total >= 20) {   // 무료 배송이 가능한지 확인

         button.show_free_shipping_icon();   // 무료 배송 버튼 노출 

      } else {

         button.hide_free_shipping_icon();   // 무료 배송 버튼 감추기

      }

   }

}

 

합계 금액이 바뀔 때마다 모든 아이콘을 업데이트하기 위해 calc_cart_total() 함수 마지막에 update_shipping_icons() 함수를 불러 준다.

 

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();   // 아이콘 업데이트 코드 추가 

}

 

 

세금 계산하기

요구사항

장바구니의 금액 합계가 바뀔 때마다 세금을 다시 계산해야 한다. 

 

function update_tax_dom() {

   set_tax_dom(shopping_cart_total * 0.10);   // 금액 합계에 10% & DOM 업데이트

}

 

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();   // 세금 업데이트 코드 추가 

}

 

 

테스트하기 쉽게 만들기

지금 코드는 비즈니스 규칙을 테스트하기 어렵다. 코드가 업데이트될 때마다 다음 내용에 대한 테스트를 만들어야 한다. 

 

1. 브라우저 설정하기

2. 페이지 로드하기

3. 장바구니에 제품 담기 버튼 클릭

4. DOM 이 업데이트될 때까지 기다리기

5. DOM 에서 값 가져오기

6. 가져온 문자열 값을 숫자로 바꾸기

7. 예상하는 값과 비교하기

 

예를 들어, 위의 update_tax_dom() 함수 같은 경우는 다음과 같다. 

 

function update_tax_dom() {

   set_tax_dom(shopping_cart_total * 0.10);   // 금액 합계에 10% & DOM 업데이트

}

 

테스트해야 하는 비즈니스 규칙 : shopping_cart_total * 0.10

이때, 테스트하기 전에 전역변수값(shopping_cart_total) 을 설정해야 하고, 결과값을 얻을 방법은 DOM 에서 값을 가져오는 방법 뿐이다.

 

이 테스트는 개선되어야 한다. * 테스트를 더 쉽게 하려면 다음 조건이 필요하다.

- DOM 업데이트와 비즈니스 규칙은 분리되어야 한다.

- 전역변수는 없어야 한다.

 

이는 함수형 프로그래밍과 잘 맞는 조건이다. 왜 잘 맞는 조건인지는 다음에서 알아보자.

 

 

재사용하기 쉽게 만들기

결제팀과 배송팀이 우리 코드를 사용하려고 한다

결제팀과 배송팀이 우리 코드를 재사용하려고 했지만, 다음 이유로 재사용할 수 없다.

 

- 장바구니 정보를 전역변수에서 읽어오고 있지만, 결제팀과 배송팀은 데이터베이스에서 장바구니 정보를 읽어 와야 한다.

- 결과를 보여주기 위해 DOM 을 직접 바꾸고 있지만, 결제팀은 영수증을, 배송팀은 운송장을 출력해야 한다.

 

결제팀과 배송팀이 재사용하려고 하는 update_shipping_icons() 함수를 다시 살펴보자.

 

function update_shipping_icons() {

   var buy_buttons = get_buy_buttons_dom();

   for (var i = 0; i < buy_buttons.length; i++) {

      var button = buy_buttons[i];

      var item = button.item;

      if (item.price + shopping_cart_total >= 20) {   // 결제팀과 배송팀에서 사용하려고 하는 비즈니스 규칙

         button.show_free_shipping_icon();   // 전역변수인 shopping_cart_total 값이 있어야 실행 가능 & DOM 이 있어야 실행 가능

      } else {

         button.hide_free_shipping_icon();   // DOM 이 있어야 실행 가능

      }

   }

   // 리턴값이 없기 때문에 결과를 받을 방법이 없다

}

 

* 재사용하기 위해서는 다음과 같은 조건이 필요하다

- 전역변수에 의존하지 않아야 한다.

- DOM 을 사용할 수 있는 곳에서 실행된다고 가정하면 안된다.

- 함수가 결과값을 리턴해야 한다. 

 

이는 함수형 프로그래밍과 잘 맞는 조건이다. 왜 잘 맞는 조건인지는 다음에서 알아보자.

 

 

액션과 계산, 데이터를 구분하기

먼저 각 함수가 액션과 계산, 데이터 중 어떤 것인지를 구분해야 한다. 그다음 어떻게 코드를 개선할 수 있는지 알 수 있다. 

액션은 A, 계산은 C, 데이터는 D 라고 표시해 보자.

 

var shopping_cart = [];   A

var shopping_cart_total = 0;   A

 

function add_item_to_cart(name, price) {   A

   shopping_cart.push({   // 전역변수를 바꾸는 것은 액션

      name : name,

      price : price

   });

   calc_cart_total();

}

 

function update_shipping_icons() {   A

   var buy_buttons = get_buy_buttons_dom();   // DOM 에서 읽는 것은 액션

   for (var i = 0; i < buy_buttons.length; i++) {

      var button = buy_buttons[i];

      var item = button.item;

      if (item.price + shopping_cart_total >= 20) {

         button.show_free_shipping_icon();   // DOM 을 바꾸는 것은 액션

      } else {

         button.hide_free_shipping_icon();   // DOM 을 바꾸는 것은 액션

      }

   }

}

 

function update_tax_dom() {   A

   set_tax_dom(shopping_cart_total * 0.10);   // DOM 을 바꾸는 것은 액션

}

 

function calc_cart_total() {   A

   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();

}

 

이 코드에는 계산이나 데이터는 없고 모든 코드가 액션이다. 

액션은 코드 전체로 퍼진다는 것을 주의해야 한다. 

 

이를 함수형 프로그래밍으로 어떻게 해결할 수 있는지 알아보자.

 

 

함수에는 입력과 출력이 있다

입력은 함수가 계산을 하기 위한 외부 정보이다. 출력은 함수 밖으로 나오는 정보나 어떤 동작이다.

함수를 부르는 이유는 결과가 필요하기 때문이고, 원하는 결과를 얻으려면 입력이 필요하다.

 

다음 함수에는 어떤 입력과 출력이 있는지 살펴보자.

 

var total = 0;

function add_to_total(amount) {   // 인자는 입력

   console.log("Old total: " + total);   // 전역변수를 읽는 것은 입력 & 콘솔에 뭔가를 찍는 것은 출력

   total += amount;   // 전역변수를 바꾸는 것은 출력

   return total;   // 리턴값은 출력

}

 

입력과 출력은 명시적이거나 암묵적일 수 있다

인자는 명시적인 입력이다. 그리고 리턴값은 명시적인 출력이다.

하지만 암묵적으로 함수로 들어가거나 나오는 정보도 있다.

 

var total = 0;

function add_to_total(amount) {   // 인자는 명시적 입력

   console.log("Old total: " + total);   // 전역변수를 읽는 것은 암묵적 입력 & 콘솔에 뭔가를 찍는 것은 암묵적 출력

   total += amount;   // 전역변수를 바꾸는 것은 암묵적 출력

   return total;   // 리턴값은 명시적 출력

}

 

함수에 암묵적 입력과 출력이 있으면 액션이 된다

함수에서 암묵적 입력과 출력이 없으면 계산이 된다. 암묵적 입력은 함수의 인자로 바꾸고, 암묵적 출력은 함수의 리턴값으로 바꾸면 된다. 

 


명시적 입력

- 인자

암묵적 입력

- 인자 외 다른 입력

명시적 출력

- 리턴값

암묵적 출력

- 리턴값 외 다른 출력

 

* 용어 설명

함수형 프로그래머는 암묵적 입력과 출력을 부수 효과라 부른다. 부수 효과는 함수가 하려고 하는 주요 기능(리턴값을 계산하는 일) 이 아니다.


 

테스트와 재사용성은 입출력과 관련 있다

테스트 측면

- DOM 업데이트와 비즈니스 규칙은 분리되어야 한다.

- 전역변수가 없어야 한다.

 

개발적 측면

- 전역변수에 의존하지 않아야 한다.

- DOM 을 사용할 수 있는 곳에서 실행된다고 가정하면 안된다.

- 함수가 결과값을 리턴해야 한다. 

 

이 제안은 모두 암묵적 입력과 출력을 없애야 한다고 말하고 있다.

 

테스트 측면 1. DOM 업데이트와 비즈니스 규칙은 분리되어야 한다.

DOM 을 업데이트하는 일은 함수에서 어떤 정보가 나오는 것이기 때문에 출력이다. 하지만 리턴값이 아니기 때문에 암묵적 출력이다. 사용자가 정보를 볼 수 있어야 하기 때문에 DOM 업데이트는 어디선가 진행이 되어야 한다. 그래서 암묵적 출력인 DOM 업데이트와 비즈니스 규칙을 분리해야 한다.

 

테스트 측면 2. 전역변수가 없어야 한다.

전역변수를 읽는 것은 암묵적 입력이고 바꾸는 것은 암묵적 출력이다. 결국 암묵적 입력과 출력을 없애야 한다고 제안한 것이다. 암묵적 입력은 인자로 바꾸고 암묵적 출력은 리턴값으로 바꾸면 된다.

 

개발적 측면1. 전역변수에 의존하지 않아야 한다. 

테스트 측면 2와 같은 내용이다. 이 역시 암묵적 입력과 출력을 없애자고 제안한 것이다.

 

개발적 측면 2. DOM 을 사용할 수 있는 곳에서 실행된다고 가정하면 안된다.

우리가 만든 함수는 DOM 을 직접 쓰고 있다. 이것은 암묵적 출력이다. 암묵적 출력은 함수의 리턴값으로 바꿀 수 있다.

 

개발적 측면 3. 함수가 결과값을 리턴해야 한다. 

암묵적 출력 대신 명시적인 출력을 사용하자고 제안하고 있다.

 

테스트와 재사용성을 높이기 위해 제안한 내용은 함수형 프로그래밍에서 다루는 명시적 입출력과 암묵적 입출력에 대한 내용이다.

이제 액션에서 계산을 빼내 보자.

 

 

액션에서 계산 빼내기

액션에서 계산을 빼내는 과정을 살펴 보자. 먼저 계산에 해당하는 코드를 분리한다. 그리고 입력값은 인자로 출력값은 리턴값으로 바꾼다.

 

원래 코드 바꾼 코드
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() {
   calc_total();
   set_cart_total_dom();
   update_shipping_icons();
   update_tax_dom();
}

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

빼낸 코드를 새로운 함수로 만들고 이름을 붙여줬다. 그리고 원래 코드에서 빼낸 부분은 새로 만든 함수를 호출하도록 고쳤다. 하지만 새 함수는 아직 액션이다. 계속 계산으로 고치는 과정을 진행해야 한다. 

 

이 리팩터링은 서브루틴 추출하기(extract subroutine) 이라 할 수 있다. 기존 코드에서 동작은 바뀌지 않는다.

 

 

function calc_total() {
   shopping_cart_total = 0;   // 출력
   for (var i = 0; i < shopping_cart.length; i++) {   // shopping_cart : 입력
      var item = shopping_cart[i];
      shopping_cart_total += item.price;   // shopping_cart_total : 출력
   }
}

 

출력은 모두 같은 전역변수값을 바꾼다. 그래서 같은 리턴값을 사용해 바꿀 수 있다. 

전역변수 대신 지역변수를 사용하도록 바꾸고 지역변수값을 리턴하도록 고치자. 그리고 원래 함수는 새 함수의 리턴값을 받아 전역변수에 할당하도록 고친다. 

 

현재 코드 암묵적 출력을 없앤 코드
function calc_cart_total() {
   calc_total();
   set_cart_total_dom();
   update_shipping_icons();
   update_tax_dom();
}

function calc_total() {
   shopping_cart_total = 0;
   for (var i = 0; i < shopping_cart.length; i++) {
      var item = shopping_cart[i];
      shopping_cart_total += item.price;
   }
}
function calc_cart_total() {
   // 리턴값을 받아 전역변수할당
   shopping_cart_total = calc_total();
   set_cart_total_dom();
   update_shipping_icons();
   update_tax_dom();
}

function calc_total() {
   var total = 0;   // 지역변수로 바꾸기
   for (var i = 0; i < shopping_cart.length; i++) {
      var item = shopping_cart[i];
      total += item.price;
   }
   return total;   // 지역변수를 리턴
}

 

암묵적 출력 두 개를 없앴다. 이제 남은 일은 암묵적 입력을 함수 인자로 바꾸는 일이다.

 

현재 코드 암묵적 입력을 없앤 코드
function calc_cart_total() {
   // 리턴값을 받아 전역변수할당
   shopping_cart_total = calc_total();
   set_cart_total_dom();
   update_shipping_icons();
   update_tax_dom();
}

function calc_total() {
   var total = 0;
   for (var i = 0; i < shopping_cart.length; i++) {
      var item = shopping_cart[i];
      total += item.price;
   }
   return total;
}
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;
}

 

이제 calc_total() 함수는 계산이다. 모든 입력은 인자이고 모든 출력은 리턴값이다.

 

모든 고민은 해결되었다. 

 

 

액션에서 또 다른 계산 빼내기

add_item_to_cart() 함수에서도 계산을 빼낼 수 있다. 앞에서 한 것처럼 빼낼 부분을 찾고 함수로 빼서 입력과 출력을 명시적으로 바꾼다.

 

원래 코드 바꾼 코드
function add_item_to_cart(name, price) {
   shopping_cart.push({
      name : name,
      price : price
   });

   calc_cart_total();

}
function add_item_to_cart(name, price) {
   add_item(name, price);
   calc_cart_total();

}

function add_item(name, price) {
   shopping_cart.push({
      name : name,
      price : price
   });
}

 

계산으로 빼낸 코드를 add_item() 함수로 만들었다. 이 함수가 동작하려면 name, price 인자가 필요하다. 그리고 빼낸 부분은 새로 만든 함수를 호출해야 한다.

 

이 리팩터링은 함수 추출하기 리팩터링이다. 새로 만든 함수는 아직 액션이다. 이제 계산으로 바꿔보자.

 

function add_item(name, price) {

   // 전역변수인 shopping_cart 를 읽고 있는 것은 입력
   shopping_cart.push({   // push() 함수로 전역변수인 배열을 바꾸고 있는 것은 출력
      name : name,
      price : price
   });
}

 

암묵적 입력과 출력을 찾았다. 이제 인자와 리턴값으로 바꿔보자.

 

현재 코드 암묵적 입력을 없앤 코드
function add_item_to_cart(name, price) {
   add_item(name, price);
   calc_cart_total();
}

function add_item(name, price) {
   shopping_cart.push({
      name : name,
      price : price
   });
}
function add_item_to_cart(name, price) {
   // 전역변수 shopping_cart 를 인자로 넘긴다.
   add_item(shopping_cart, name, price);
   calc_cart_total();
}

// 전역변수 대신 인자를 사용하도록 cart 를 추가한다.
function add_item(cart, name, price) {
   cart.push({
      name : name,
      price : price
   });
}

 

호출하는 쪽은 전역변수를 인자로 넘기도록 고친다. 암묵적인 입력을 인자로 바꿔 명시적인 입력이 되었다.

 

이제 암묵적 출력을 바꿔보자.

 

현재 코드 암묵적 출력을 없앤 코드
function add_item_to_cart(name, price) {
   add_item(shopping_cart, name, price);
   calc_cart_total();
}

function add_item(cart, name, price) {
    cart.push({
      name : name,
      price : price
   });
}
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;
}

 

복사본을 만들고 복사본에 제품을 추가해서 리턴했다. 그리고 호출하는 코드에서는 리턴값을 받아 전역변수에 할당했다. 이제 암묵적 출력값이 리턴값으로 바뀌었다. 

 

계산으로 바꾸는 작업이 끝났다. add_item() 함수는 암묵적 입력이나 출력이 없는 계산이다.

 

 


재사용성을 높이고 쉽게 테스트할 수 있는 것이 함수형 프로그래밍으로 얻을 수 있는 전부인가 ? 

- 그외에도 동시성이나 설계, 데이터 모델링 측면에서 좋은 점들이 있다. 


 

전체적인 코드에 대한 리팩터링 작업(추가 예정)

 

 

 

출처 : 쏙쏙 들어오는 함수형 코딩(Grokking Simplicity: Taming complex software with functional thinking)