
쏙쏙 들어오는 함수형 코딩 Chapter 13~18
Chapter 13 : 함수형 도구 체이닝
#앞장에서 함수형 도구로 반복문에서 하려는 일과 반복하는 일을 분리했다. 하지만 계산이 더 복잡해지면 함수형 도구 하나로 작업할 수 없다. 이 장에서는 여라 단계를 하나로 엮은 체인으로 복합적 계산을 표현하는 방법을 알아본다.
요구사항
#각각의 우수 고객(3개 이상 구매)의 구매 중 가장 비싼 구매를 알고 싶다!
단계 나누기
#- 우수 고객을 거른다. (filter)
- 우수 고객을 가장 비싼 구매로 바꾼다. (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이 난다.
바로 암묵적 인자를 두 가지 사용하는 것이다. Size 와 increment 말이다!
그럼 명시적 인자로 바꿔보자
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);
});
});
}다이어그램
#
첫번째 클릭 시 일어나는 액션이 다 끝나지않은 상태에서 두번째 클릭 액션이 실행되어 생기는 문제이다.
- 첫번째 타임라인의 shipping_ajax() 의 응답이 조금 늦게 도착함.
- 두번째 타임라인의 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);
}하지만 아직 버그는 남아 있다. 다음 장에서 해결해보자..
좋은 타임라인의 원칙
#- 타임라인은 적을수록 이해하기 쉽다.
- 타임라인은 짧을수록 이해하기 쉽다.
- 공유하는 자원이 적을수록 이해하기 쉽다.
- 자원을 공유한다면 서로 조율해야 한다.
- 시간을 일급으로 다룬다.
비동기 호출 원칙
#비동기 호출에서 명시적인 출력을 위해 리턴값 대신 콜백을 사용할 수 있다.
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);함수형 프로그래밍과 변경 가능한 상태
#중요한 것은 상태를 가능한 한 안전하게 사용하는 것이다.
셀은 변경할 수 있지만 변경 불가능한 변수에 값을 담아두기 때문에 전역변수보다 더 안전하다.
ValueCell 의 update 사용하면 현재 값을 항상 올바르게 유지할 수 있다.
왜냐 ? update 는 계산을 넘기기 때문이다. 계산은 현재 값을 받아 새로운 값을 리턴한다.
ValueCell 는 순서를 보장하지는 않는다. 하지만 어떤 값이 저장되어도 그 값이 항상 올바른 값이라는 것을 보장한다.
ValueCell는 Redux store, Recoil atom, React useState 등과 컨셉이 매우 유사하다.
반응형 아키텍처가 바꾼 시스템의 결과
#- 원인과 효과가 결합된 것을 분리한다.
- 여러 단계를 파이프라인으로 처리한다.
- 타임라인이 유연해진다.
어니언 아키텍처
#현실세계와 상호작용하기 위한 서비스 구조를 만드는 방법이다.
어니언 아키텍처는 서비스 전체를 구성하는 데 사용하기 때문에 바깥 세계와 상호작용하는 부분을 다룬다.
어니언 아키텍처는 양파 모양을 하고 있고 아래 계층들로 구성되어 있다.
인터랙션 계층
#- 바깥세상에 영향을 주거나 받는 액션
도메인 계층
#- 비즈니스 규칙을 정의하는 계산
언어 계층
#- 언어 유틸리티와 라이브러리
어니언 아키텍처 규칙
#- 현실 세계와 상호작용은 인터렉션 계층에서 해야한다.
- 계층에서 호출하는 방향은 중심 방향이다.
- 계층은 외부에 어떤 계층이 있는 지 모른다.
어니언 아키텍처

어니언 아키텍처는 인터렉션 계층을 바꾸기 쉽다. 그래서 도메인 계층을 재사용하기 좋다.
19장은 이전 Chapter들을 돌아보는 Chapter로 생략하겠다!