\(@^0^@)/

[BOOK] 쏙쏙 들어오는 함수형 코딩 4 본문

BOOKS/Grokking simplicity 함수형 코딩

[BOOK] 쏙쏙 들어오는 함수형 코딩 4

minjuuu 2022. 8. 1. 21:41
728x90

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

절차적인 방법으로 구현한 쇼핑몰 페이지의 코드를 함수형으로 리팩터링 하기 위해서 액션, 계산, 데이터로 구분해보자.

액션은 코드 전체로 퍼진다.
어떤 함수 안에 액션이 하나만 있어도 그 함수 전체가 액션이 된다.

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

 

  • 정보는 입력을 통해 함수 안으로 들어간다.
  • 정보나 어떤 효과는 출력으로 함수 밖으로 나온다.

 

  • 입력과 출력은 명시적이거나 암묵적일 수도 있다.
    • 인자는 명시적인 입력이다.
    • 리턴 값은 명시적인 출력이다.
    • 하지만 암묵적으로 함수로 들어가거나 나오는 정보도 있다.
let total = 0;
function add_to_total(amount) { // 인자는 명시적 입력
  console.log("합계 :" + total); // 전역변수를 읽는 것은 암묵적 입력
  // 콘솔에 찍는 것은 암묵적 출력
  total += amount;
  // 전역변수를 바꾸는 것도 암묵적 출력
  return total;
  // 리턴값은 명시적 출력
}

 

  • 함수에 암묵적 입력과 출력이 있으면 액션이 된다.
    • 함수에서 암묵적 입력과 출력을 없애면 계산이 된다.
    • 암묵적 입력은 함수의 인자로 바꾸고, 암묵적 출력은 함수의 리턴 값으로 바꾸면 된다.
함수형 프로그래머는 암묵적 입력과 출력을 부수 효과라고 부른다.
부수 효과는 함수가 하려고 하는 주요 기능(리턴 값을 계산하는 일)이 아니다.

  • 명시적 입력
    • 인자
  • 암묵적 입력
    • 인자 외 다른 입력
  • 명시적 출력
    • 리턴 값
  • 암묵적 출력
    • 리턴 값 외 다른 출력

테스트와 재사용성을 위해 고민하기 위한 제안들

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

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

2. 전역 변수가 없어야 한다 (의존하지 않아야 한다)

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

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

암묵적 출력은 함수의 리턴 값으로 바꿀 수 있다.

4. 함수가 결괏값을 리턴해야 한다.

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


장바구니 액션에서 계산 빼내기

1. 계산에 해당하는 코드를 분리한다.
2. 입력값은 인자로, 출력 값은 리턴 값으로 바꾼다.

/* 원래의 코드 */
function calc_cart_total() {
  shopping_cart_total = 0;
  for(let i = 0; i < shopping_cart.length; i++) {
    let item = shopping_cart[i];
    shopping_cart_total += item.price;
  }
  
  set_cart_total_dom();
  update_shipping_icons();
  update_tax_dom();
}
/* 바꾼 코드*/
function calc_cart_total() {
  set_cart_total_dom();
  update_shipping_icons();
  update_tax_dom();
}

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

빼낸 코드를 새로운 함수로 만들고 이름을 붙여주고, 원래 코드에서 빼낸 부분은 새로 만든 함수를 호출하도록 고쳤음.
하지만 새 함수는 아직 액션이다. 새 함수를 계산으로 리팩터링 해야 함.
위의 리팩터링은 서브루틴 추출하기(extract subroutine)라고 할 수 있다. 기존 코드에서 동작은 바뀌지 않았다.


새 함수는 아직 액션이니, 계산으로 바꿔보자.
계산으로 바꾸려면 먼저 어떤 입력과 출력이 있는지 확인해야 한다.

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

이 함수에는 출력 두 개와 입력 하나가 있다.
shopping_cart_total 전역 변숫값을 바꾸는 것이 출력이고, shopping_cart 전역 변숫값을 읽어오는 것이 입력이다.
여기에 있는 입력과 출력은 모두 암묵적이기 때문에, 명시적인 입출력으로 바꿔야 계산이 된다.

출력은 모두 같은 전역 변숫값을 바꾼다면, 같은 리턴 값을 사용할 수 있다.
전역 변수 대신 지역변수를 사용하도록 바꾸고 지역 변숫값을 리턴하도록 고친다.
그리고 원래 함수는 새 함수의 리턴 값을 받아 전역 변수에 할당하도록 고친다.

/* 바꿔야 할 코드*/
function calc_cart_total() {
  set_cart_total_dom();
  update_shipping_icons();
  update_tax_dom();
}

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

 

cart라는 인자를 추가하고 함수 안에서 cart 인자를 사용하도록 고쳐 암묵적 입력도 없애보자.

/* 암묵적 입력을 없앤 코드*/
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) {
  let total = 0;
  for(let i = 0; i < cart.length; i++) {
    let item = cart[i];
    total += item.price;
  }
  return total;
}

아이템 추가 액션에서 계산 빼내기

/* 원래의 코드 */
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
  })
}

하지만 이 작업은 함수 추출 리팩터링이므로, 새로 만든 함수는 아직 액션이다.
전역 변수인 shopping_cart 배열을 바꾸기 때문.

/* 암묵적 입력을 없앤 코드 */
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
  })
}

원래 shopping_cart 전역 변수를 직접 사용하고 있었지만, add_item()에 인자를 추가해서 인자를 사용하도록 바꾸었다.
따라서 암묵적인 입력을 인자로 바꿔 명시적인 입력이 되었다.

 

우리가 확인했던 출력은 shopping_cart에 있는 배열을 바꾸는 부분이었다.
이 값을 바꾸는 대신 복사본을 만들고, 복사본에 추가해 리턴해야 한다.

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

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

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

어떤 값을 바꿀 때 그 값을 복사해서 바꾸는 방법은 불변성을 구현하는 방법 중 하나이다.
이 방법을 카피-온-라이트 (copy-on-write)라고 한다.
자바스크립트에서 배열을 직접 복사하는 방법이 없다.
그래서 이 책에서는. slice() 메서드를 사용.

계산 추출을 단계별로 알아보기

1. 계산 코드를 찾아 빼낸다.

빼낼 코드를 찾는다. 코드를 추출해 새로운 함수를 만들어 리팩터링 한다.
새 함수에 인자가 필요하다면 추가한다. 원래 코드에서 빼낸 부분에 새 함수를 부르도록 바꾼다.

2. 새 함수에 암묵적 입력과 출력을 찾는다.

새 함수에 암묵적 입력과 출력을 찾는다.
암묵적 입력은 함수를 부르는 동안 결과에 영향을 줄 수 있는 것. 암묵적 출력은 함수 호출의 결과로 영향을 받는 것.
함수 인자를 포함해 함수 밖에 있는 변수를 읽거나 데이터베이스에서 값을 가져오는 것은 입력이다.
리턴 값을 포함해 전역 변수를 바꾸거나 공유 객체를 바꾸거나, 웹 요청을 보내는 것은 출력이다.

3. 암묵적 입력은 인자로, 암묵적 출력은 리턴 값으로 바꾼다.

한 번에 하나씩 입력은 인자로 출력은 리턴 값으로 바꾼다.
새로운 리턴 값이 생겼다면 호출하는 코드에서 함수의 결과를 변수에 할당해야 할 수도 있다.
여기서 인자와 리턴 값은 바뀌지 않는 불변 값이라는 것이 중요하다.
리턴 값이 나중에 바뀐다면 암묵적 출력, 인자로 받은 값이 바뀔 수 있다면 암묵적 입력이다.


세금 계산 액션에서 계산 빼내기

 

/* 기존 코드 */
function update_tax_dom() {
	set_tax_dom(shopping_cart_total * 0.10);
}
/* 바꾼 코드 */
function update_tax_dom() {
	set_tax_dom(calc_tax(shopping_cart_total));
}

function calc_tax(amount) {
	return amount * 0.1;
}

배송비 계산 액션에서 계산 빼내기

/* 기존 코드 */
function update_shipping_icons() {
	var buy_buttons = get_buy_buttons_dom();

	for (let i = 0; i < buy_buttons.length; i++) {
		let button = buy_buttons[i];
		let item = button.item;
		if (item.price + shopping_cart_total >= 20) {
			button.show_free_shipping_icon(); 
		} else {
			button.hide_free_shipping_icon(); 
		}
	}
}
/* 바꾼 코드 */
function update_shipping_icons() {
	var buy_buttons = get_buy_buttons_dom();

	for (let i = 0; i < buy_buttons.length; i++) {
		let button = buy_buttons[i];
		let item = button.item;
		if (gets_free_shipping(shopping_cart_total, item.price)) {
			button.show_free_shipping_icon();
		} else {
			button.hide_free_shipping_icon();
		}
	}
}

function gets_free_shipping(price, item_price) {
	return price + item_price >= 20;
}

고친 전체 코드를 다시 보자

기존의 코드보다 액션이 훨씬 줄어들고, 계산이 늘어난 것을 볼 수 있다.


요점 정리

  • 액션은 암묵적인 입력 또는 출력을 갖고 있다.
  • 계산의 정의에 따르면 계산은 암묵적인 입력이나 출력이 없어야 한다.
  • 공유 변수(전역 변수 같은)는 일반적으로 암묵적 입력 또는 출력이 된다.
  • 암묵적 입력은 인자로 바꿀 수 있다.
  • 암묵적 출력은 리턴 값으로 바꿀 수 있다.
  • 함수형 원칙을 적용하면 액션은 줄어들고 계산은 늘어난다.
728x90