쏙쏙 들어오는 함수형 코딩 Chapter 13~18

쏙쏙 들어오는 함수형 코딩 Chapter 13~18

Chapter 13 : 함수형 도구 체이닝

앞장에서 함수형 도구로 반복문에서 하려는 일과 반복하는 일을 분리했다. 하지만 계산이 더 복잡해지면 함수형 도구 하나로 작업할 수 없다. 이 장에서는 여라 단계를 하나로 엮은 체인으로 복합적 계산을 표현하는 방법을 알아본다.

요구사항

각각의 우수 고객(3개 이상 구매)의 구매 중 가장 비싼 구매를 알고 싶다!

단계 나누기

  1. 우수 고객을 거른다. (filter)
  2. 우수 고객을 가장 비싼 구매로 바꾼다. (map)

예시

function biggestPurchasesBestCustomers(customers) {
  var bestCustomers = filter(customers, function(customer) {
    return customer.purchases.length >= 3;
  });
  var biggestPurchases = map(bestCustomers, function(customer) {
    return maxKey(customer.purchases, {total: 0}, function(purchase) {
      return purchase.total;
    });
  });
  return biggestPurchases;
}

function maxKey(array, init, f) {
  return reduce(array, init, function(biggestSoFar, element) {
    if(f(biggestSoFar) > f(element)) 
      return biggestSoFar;
    else
      return element;
  });

항등 함수 : 인자를 받은 값을 그대로 리턴하는 함수

위 예시를 리팩토링해 체인을 명확하게 만들어보자!

function biggestPurchasesBestCustomers(customers) {
  var bestCustomers = filter(customers, function(customer) {
    return customer.purchases.length >= 3;
  });
  var biggestPurchases = map(bestCustomers, function(customer) {
    return maxKey(customer.purchases, {total: 0}, function(purchase) {
      return purchase.total;
    });
  });
  return biggestPurchases;
}

function biggestPurchasesBestCustomers(customers) {
  var bestCustomers    = filter(customers, isGoodCustomer);
  var biggestPurchases = map(bestCustomers, getBiggestPurchase);
  return biggestPurchases;
}

function isGoodCustomer(customer) {
  return customer.purchases.length >= 3;
}

function getBiggestPurchase(customer) {
  return maxKey(customer.purchases, {total: 0}, getPurchaseTotal);
}

function getPurchaseTotal(purchase) {
  return purchase.total;
}

단계에 이름을 붙이고, 콜백에 이름을 붙여 더 명확해졌다.

반복문을 함수형 도구로 리팩터링하기

var answer = [];
var window = 5;
for(var i = 0; i < array.length; i++) {
  var sum = 0;
  var count = 0;
  for(var w = 0; w < window; w++) {
    var idx = i + w;
    if(idx < array.length) {
      sum += array[idx];
      count += 1;
    }
  }
  answer.push(sum/count);
}

위 코드를 함수형 도구로 바꿔보자!

var window = 5;
var indices = range(0, array.length);

var windows = map(indices, function(i) {
  return array.slice(i, i + window); // 하위 배열 만들기
});

var answer = map(windows, average); // 전에 만들었던 average 함수 사용

function range(start, end) {
  var ret = [];
  for(var i = start; i < end; i++)
    ret.push(i);
  return ret;
}

체이닝 팁 요약

데이터 만들기

배열 일부에 대해 동작하는 반복문이 있다면 배열 일부를 새로운 배열로 나눌 수 있다. 그리고 map, filter, reduce 등 함수형 도구를 사용하자

배열 전체를 다루기

어떻게 하면 반복문을 대신해 전체 배열을 한 번에 처리할 수 있을지 생각해보자

작은 단계로 나누기

알고리즘이 한 번에 너무 많은 일을 한다고 생각된다면 여러 단계로 나눠보자

Chapter 14 : 중첩된 데이터에 함수형 도구 사용하기

중첩된 구조는 자주 사용한다. 하지만 중첩된 데이터를 불변 데이터로 다루는 것은 어렵다. 이 장에서는 중첩된 데이터를 쉽게 다룰 수 있는 함수형 도구를 소개한다.

update() 도출하기

function incrementField(item, field) {
  var value = item[field];
  var newValue = value + 1;
  var newItem = objectSet(item, field, newValue);
  return newItem;
}

// -> 

function incrementField(item, field) {
  return updateField(item, field, function(value) {
  return value + 1;
  });
}

function updateField(item, field, modify) {
  var value = item[field];
  var newValue = modify(value);
  var newItem = objectSet(item, field, newValue);
  return newItem;
}

// ->

function update(object, key, modify) {
  var value = object[key];
  var newValue = modify(value);
  var newObject = objectSet(object, key, newValue);
  return newObject;
}

암묵적 인자 드러내기함수 본문을 콜백으로 바꾸기 리팩터링으로 update 함수를 도출했다.

사용 예시

var employee = {
  name: "Kim",
  salary: 120000
};

function raise10Percent(salary) {
  return salary * 1.1;
}

update(employee, 'salary', raise10Percent)

중첩된 데이터에 update() 사용하기

update 함수가 객체 바로 아래에 있는 데이터에는 잘 동작하는 것 같다. 그런데 중첩된 구조에서는 어떨까?

예시

리팩토링 전

var shirt = {
  name: "shirt",
  price: 13,
  options: {
    color: "blue",
    size: 3
  }
};

function incrementSize(item) {
  var options = item.options;
  var size = options.size;
  var newSize = size + 1;
  var newOptions = objectSet(options, 'size', newSize);
  var newItem = objectSet(item, 'options', newOptions);
  return newItem;
}

리팩토링 후

function incrementSize(item) {
  var options = item.options;
  var newOptions = update(options, 'size', increment);
  var newItem = objectSet(item, 'options', newOptions);
  return newItem;
}


->

function incrementSize(item) {
  return update(item, 'options', function(options) {
    return update(options, 'size', increment);
  });
}

두 번의 리팩토링을 통해 중첩된 데이터에 update()를 사용할 수 있었다.

하지만 incrementSize 함수 에서는 두 가지 코드 smell이 난다.

바로 암묵적 인자를 두 가지 사용하는 것이다. Sizeincrement 말이다!

그럼 명시적 인자로 바꿔보자

function incrementOption(item, option) {
  return update(item, 'options', function(options) {
    return update(options, option, increment);
  });
}

function updateOption(item, option, modify) {
  return update(item, 'options', function(options) {
    return update(options, option, modify);
  });
}

잘 만든 것 같은데 같은 냄새가 또 생겼다! 바로 options 이다.

다시 리팩터링 해보자.

update2() 도출하기

 function update2(object, key1, key2, modify) {
  return update(object, key1, function(value1) {
    return update(value1, key2, modify);
  });
}

이제 update2 함수는 두 단계로 중첩된 어떤 객체에도 쓸 수 있는 함수이다.

원래 코드랑 update2 함수를 사용한 코드를 비교해보자!

// 기존 코드
function incrementSize(item) {
  var options = item.options;
  var size = options.size;
  var newSize = size + 1;
  var newOptions = objectSet(options, 'size', newSize);
  var newItem = objectSet(item, 'options', newOptions);
  return newItem;
}

/// update2() 사용
function incrementSize(item) {
  return update2(item, 'options', 'size', function(size) {
    return size + 1;
  });
}

update3() 도출하기

update 함수와 update2 함수를 도출한 것처럼 update3 함수로 도출할 수 있다.

function update3(object, key1, key2, key3, modify) {
  return update(object, key1, function(object2) {
    return update2(object2, key2, key3, modify);
  });
}

nestedUpdate() 도출하기

update3 함수 깊이(depth)라는 인자를 추가해보자

function updateX(object, depth, key1, key2, key3, modify) {
  return update(object, key1, function(value1) {
    return updateX(value1, depth-1, key2, key3, modify);
  });
}

// ->

function updateX(object, keys, modify) {
  var key1 = keys[0];
  var restOfKeys = drop_first(keys);
  return update(object, key1, function(value1) {
    return updateX(value1, restOfKeys, modify);
  });
}

depth 인자와 실제 키 개수는 달라질 수 있어서 버그가 생길 수 있으니 keys 를 사용해서 다시 리팩터링한다.

이제 정상적으로 작동할 것처럼 보이지만 아직 keys 배열 길이가 0일 때 처리를 안했다. 해보자!

function updateX(object, keys, modify) {
  if(keys.length === 0)
    return modify(object);
  var key1 = keys[0];
  var restOfKeys = drop_first(keys);
  return update(object, key1, function(value1) {
    return updateX(value1, restOfKeys, modify);
  });
}

// ->

function nestedUpdate(object, keys, modify) {
  if(keys.length === 0)
    return modify(object);
  var key1 = keys[0];
  var restOfKeys = drop_first(keys);
  return update(object, key1, function(value1) {
    return nestedUpdate(value1, restOfKeys, modify);
  });
}

이름을 nestedUpdate 로 바꾸었다. 이제 여러 단계로 중첩된 객체를 마음껏 조작할 수 있다.

Chapter 15 : 타임라인 격리하기

이번 장에서는 시간에 따라 실행되는 액션의 순서를 나타내기 위해 타임라인 다이어그램에 대해 알아보자

예시

제품을 장바구니에 추가할 때 빠르게 두 번 클릭하면 버그가 생기는 코드가 있다.

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

function calc_cart_total() {
  total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      update_total_dom(total);
    });
  });
}

다이어그램

첫번째 클릭 시 일어나는 액션이 다 끝나지않은 상태에서 두번째 클릭 액션이 실행되어 생기는 문제이다.

  1. 첫번째 타임라인의 shipping_ajax() 의 응답이 조금 늦게 도착함.
  2. 두번째 타임라인의 cart 읽기 가 시작됨.

어떻게 코드를 바꿔서 문제를 해결할 수 있을까?

일단 암묵적 인자부터 없애자!

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

function add_item_to_cart(name, price, quant) {
  cart = add_item(cart, name, price, quant);
  calc_cart_total(cart, update_total_dom);
}

하지만 아직 버그는 남아 있다. 다음 장에서 해결해보자..

좋은 타임라인의 원칙

  1. 타임라인은 적을수록 이해하기 쉽다.
  2. 타임라인은 짧을수록 이해하기 쉽다.
  3. 공유하는 자원이 적을수록 이해하기 쉽다.
  4. 자원을 공유한다면 서로 조율해야 한다.
  5. 시간을 일급으로 다룬다.

비동기 호출 원칙

비동기 호출에서 명시적인 출력을 위해 리턴값 대신 콜백을 사용할 수 있다.

Chapter 16 : 타임라인 사이에 자원 공유하기

이전 장의 장바구니 버그를 다시 보자

다이어그램

위 다이어그램은 DOM을 업데이트하는 두 액션이 실행할 때 가능한 순서이다.

왼쪽부터 [왼쪽 먼저], [오른쪽 먼저], [동시에] 이다.

자바스크립트는 싱글 스레드 모델이라 동시에 실행될 수는 없다 따라서 가능한 순서는 [왼쪽 먼저], [오른쪽 먼저] 이다.

자바스크립트에서 큐 만들기

우리가 해결해야하는 문제는 오른쪽이 먼저 실행되는 것을 막는 것이다.

큐를 활용해서 막아보자!

// 기존 코드
function add_item_to_cart(item) {
  cart = add_item(cart, item);
  calc_cart_total(cart, update_total_dom);
}

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

// -> Step 1

function add_item_to_cart(item) {
  cart = add_item(cart, item);
  update_total_queue(cart);
}

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

var queue_items = [];

function update_total_queue(cart) {
  queue_items.push(cart);
}

// -> Step 2

function add_item_to_cart(item) {
  cart = add_item(cart, item);
  update_total_queue(cart);
}

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

var queue_items = [];

function runNext() {
  var cart = queue_items.shift();
  calc_cart_total(cart, update_total_dom);
}

function update_total_queue(cart) {
  queue_items.push(cart);
  setTimeout(runNext, 0);
}

// -> Step 3

function add_item_to_cart(item) {
  cart = add_item(cart, item);
  update_total_queue(cart);
}

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

var queue_items = [];
var working = false;

function runNext() {
  if(working)
    return;
  working = true;
  var cart = queue_items.shift();
  calc_cart_total(cart, update_total_dom);
}

function update_total_queue(cart) {
  queue_items.push(cart);
  setTimeout(runNext, 0);
}

// -> Step 4

var queue_items = [];
var working = false;

function runNext() {
  if(working)
    return;
  working = true;
  var cart = queue_items.shift();
  calc_cart_total(cart, function(total) {
    update_total_dom(total);
    working = false;
    runNext();
  });
}

function update_total_queue(cart) {
  queue_items.push(cart);
  setTimeout(runNext, 0);
}

// -> End

function Queue() {
  var queue_items = [];
  var working = false;

  function runNext() {
    if(working)
      return;
    if(queue_items.length === 0)
      return;
    working = true;
    var cart = queue_items.shift();
    calc_cart_total(cart, function(total) {
      update_total_dom(total);
      working = false;
      runNext();
    });
  }

  return function(cart) {
    queue_items.push(cart);
    setTimeout(runNext, 0);
  };
}

var update_total_queue = Queue();

여러 단계에 걸쳐 큐를 만들었다.

큐를 재사용할 수 있도록 만들기

function Queue(worker) {
  var queue_items = [];
  var working = false;

  function runNext() {
    if(working)
      return;
    if(queue_items.length === 0)
      return;
    working = true;
    var item = queue_items.shift();
    worker(item.data, function(val) {
      working = false;
      setTimeout(item.callback, 0, val);
      runNext();
    });
  }

  return function(data, callback) {
    queue_items.push({
      data: data,
      callback: callback || function(){}
    });
    setTimeout(runNext, 0);
  };
}

function calc_cart_worker(cart, done) {
  calc_cart_total(cart, function(total) {
    update_total_dom(total);
    done(total);
  });
}

var update_total_queue = Queue(calc_cart_worker);

다이어그램

타임라인은 위와 같이 표현된다. 이로써 장바구니 버그가 드디어 해결되었다.

Chapter 17 : 타임라인 조율하기

장바구니에 큐를 적용해서 배포한 지 일주일이 지났다. 그 후 UI 속도를 개선해달라는 요청이 많이 있었다.

그리고 최적화를 했는 데 버그가 생겼다.

// 원래 코드
function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
    });
  });
}

// 최적화한 코드
function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
    });
    shipping_ajax(cart, function(shipping) {
      total += shipping;
      callback(total);
  });
}

닫은 괄호가 옮겨졌다. cost_ajax 의 콜백 안에서 shipping_ajax 가 호출되는 대신, cost_ajax , shipping_ajax 가 동시에 실행되게 하여 속도를 높였다. 하지만 버그가 있다.

다이어그램

위와 같은 타임라인을 가졌기 때문에 오른쪽이 먼저 실행 될때 버그가 발생한다.

우리가 원하는 결과는 아래와 같다

두 콜백이 서로 끝나기를 기다리고 다음 작업을 수행한다. 이 점선을 컷(cut)이라고 부른다.

코드에 Cut() 적용하기

function Cut(num, callback) {
  var num_finished = 0;
  return function() {
    num_finished += 1;
    if(num_finished === num)
      callback();
  };
}

// example
var done = Cut(3, function() {
  console.log("3 timelines are finished");
});
  
done();
done();
done();

자바스크립트의 스레드는 하나이다. Cut 함수는 이런 장점을 활용해 변경할 수 있는 값을 안전하게 공유한다.

적용

// 적용 전

function calc_cart_total(cart, callback) {
  var total = 0;
  cost_ajax(cart, function(cost) {
    total += cost;
  });
  shipping_ajax(cart, function(shipping) {
    total += shipping;
    callback(total);
  });
}

// Cut() 적용

function calc_cart_total(cart, callback) {
  var total = 0;
  var done = Cut(2, function() {
    callback(total);
  });
  cost_ajax(cart, function(cost) {
    total += cost;
    done();
  });
  shipping_ajax(cart, function(shipping) {
    total += shipping;
    done();
  });
}

이로써 동시에 실행하면서 속도도 개선하고 순서대로 실행하는 것과 같은 올바른 결과도 얻었다.

JustOnce()

function sendAddToCartText(number) {
  sendTextAjax(number, "Thanks for adding something to your cart. Reply if you have any questions!");
}

function JustOnce(action) {
  var alreadyCalled = false;
  return function(a, b, c) {
    if(alreadyCalled) return;
    alreadyCalled = true;
    return action(a, b, c);
  };
}

sendAddToCartTextOnce("555-555-5555-55");
sendAddToCartTextOnce("555-555-5555-55");
sendAddToCartTextOnce("555-555-5555-55");
sendAddToCartTextOnce("555-555-5555-55");

딱 한 번만 호출하는 함수도 위와 같이 만들 수 있다.

Chapter 18 : 반응형 아키텍처와 어니언 아키텍처

반응형 아키텍처

애플리케이션을 구조화하는 방법이다.

반응형 아키텍처는 코드에 나타난 순차적 액션의 순서를 뒤집는다.

반응형 아키텍처의 핵심 원칙은 이벤트에 대한 반응으로 일어날 일을 지정하는 것이다.

셀은 일급 상태입니다.

우리가 살펴본 장바구니 예제에서 전역 상태는 장바구니이다. 필요한 것은 장바구니가 변경될 때 Y를 하는 것이다.

상태를 일급 함수로 만들자. 전역변수를 몇 가지 동작과 함께 객체로 만들자.

// 기존 코드

var shopping_cart = {};

function add_item_to_cart(name, price) {
  var item = make_cart_item(name, price);
  shopping_cart = add_item(shopping_cart, item);
  var total = calc_total(shopping_cart);
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart);
  update_tax_dom(total);
}

// 셀을 적용한 코드

function ValueCell(initialValue) {
  var currentValue = initialValue;
  return {
    val: function() {
      return currentValue;
    },
    update: function(f) {
      var oldValue = currentValue;
      var newValue = f(oldValue);
      currentValue = newValue;
    }
  };
}

var shopping_cart = ValueCell({});

function add_item_to_cart(name, price) {
  var item = make_cart_item(name, price);
  shopping_cart.update(function(cart) {
    return add_item(cart, item);
  });
  var total = calc_total(shopping_cart.val());
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart.val());
  update_tax_dom(total);
}

다음은 ValueCell 코드에 감시자 개념을 추가하자

function ValueCell(initialValue) {
  var currentValue = initialValue;
  var watchers = [];
  return {
    val: function() {
      return currentValue;
    },
    update: function(f) {
      var oldValue = currentValue;
      var newValue = f(oldValue);
      if(oldValue !== newValue) {
        currentValue = newValue;
        forEach(watchers, function(watcher) {
          watcher(newValue);
        });
      }
    },
    addWatcher: function(f) {
      watchers.push(f);
    }
  };
}

감시자 개념을 사용해 셀이 바뀔 때 배송 아이콘을 갱신할 수 있다.

// 기존 코드

var shopping_cart = ValueCell({});

function add_item_to_cart(name, price) {
  var item = make_cart_item(name, price);
  shopping_cart.update(function(cart) {
    return add_item(cart, item);
  });
  var total = calc_total(shopping_cart.val());
  set_cart_total_dom(total);
  update_shipping_icons(shopping_cart.val());
  update_tax_dom(total);
}

// 감시자 적용한 코드

var shopping_cart = ValueCell({});

function add_item_to_cart(name, price) {
  var item = make_cart_item(name, price);
  shopping_cart.update(function(cart) {
    return add_item(cart, item);
  });
  var total = calc_total(shopping_cart.val());
  set_cart_total_dom(total);
  update_tax_dom(total);
}

shopping_cart.addWatcher(update_shipping_icons);

함수형 프로그래밍과 변경 가능한 상태

중요한 것은 상태를 가능한 한 안전하게 사용하는 것이다.

셀은 변경할 수 있지만 변경 불가능한 변수에 값을 담아두기 때문에 전역변수보다 더 안전하다.

ValueCellupdate 사용하면 현재 값을 항상 올바르게 유지할 수 있다.

왜냐 ? update 는 계산을 넘기기 때문이다. 계산은 현재 값을 받아 새로운 값을 리턴한다.

ValueCell 는 순서를 보장하지는 않는다. 하지만 어떤 값이 저장되어도 그 값이 항상 올바른 값이라는 것을 보장한다.

ValueCellRedux store, Recoil atom, React useState 등과 컨셉이 매우 유사하다.

반응형 아키텍처가 바꾼 시스템의 결과

  1. 원인과 효과가 결합된 것을 분리한다.
  2. 여러 단계를 파이프라인으로 처리한다.
  3. 타임라인이 유연해진다.

어니언 아키텍처

현실세계와 상호작용하기 위한 서비스 구조를 만드는 방법이다.

어니언 아키텍처는 서비스 전체를 구성하는 데 사용하기 때문에 바깥 세계와 상호작용하는 부분을 다룬다.

어니언 아키텍처는 양파 모양을 하고 있고 아래 계층들로 구성되어 있다.

인터랙션 계층

  • 바깥세상에 영향을 주거나 받는 액션

도메인 계층

  • 비즈니스 규칙을 정의하는 계산

언어 계층

  • 언어 유틸리티와 라이브러리

어니언 아키텍처 규칙

  1. 현실 세계와 상호작용은 인터렉션 계층에서 해야한다.
  2. 계층에서 호출하는 방향은 중심 방향이다.
  3. 계층은 외부에 어떤 계층이 있는 지 모른다.

어니언 아키텍처

어니언 아키텍처는 인터렉션 계층을 바꾸기 쉽다. 그래서 도메인 계층을 재사용하기 좋다.

19장은 이전 Chapter들을 돌아보는 Chapter로 생략하겠다!