# Chapter13. 함수형 도구 체이닝
# 1. reduce
, maxKey
함수 적용
- before
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function(customer) {
return reduce(customer.purchases, { total: 0 }, function(biggestSoFar, purchase) {
if (biggestSoFar.total > purchase.total) {
return biggestSoFar;
} else {
return purchase;
};
});
});
return biggestPurchases;
}
- after
- 위 코드보다 조금 개선되었지만 아직 코드에 중첩된 리턴 구문이 있는 콜백이 있다.
// maxKey 함수의 세 번째 인자로 어떤 값을 비교할지 콜백 function으로 전달한다.
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;
}
maxKey()
함수로max()
함수를 만들 수 있다.maxKey()
는 비교하는 값을 자유롭게 선택해서 최댓값을 구할 수 있지만,max()
는 값을 직접 비교해야 한다.- 호출 그래프상에서
maxKey()
가max()
보다 아래 위치한다.maxKey()
는max()
보다 더 일반적인 함수다.max()
는maxKey()
의 특별한 버전이라고 볼 수 있다.
// maxKey 함수로 max 함수 만들기
// 아래처럼 인자로 받은 값을 그대로 리턴하는 함수를 항등 함수(identify function)라고 한다.
function max(array, init) {
return maxKey(array, init, function(x) {
return x;
})
}
# 2. 체인을 명확하게 만들기
체이닝(chaining)
단계들을 조합해 하나의 쿼리로 만드는 것으로 여러 단계를 하나로 조합하는 것을 말한다.
# (1) 단계에 이름 붙이기
- 각 단계의 고차 함수를 빼내 이름을 붙인다.
- 각 단계에 이름을 붙이면 훨씬 명확해지고 각 단계에 숨어 있던 두 함수의 구현도 알아보기 쉽다.
- 하지만 인라인으로 정의된 콜백 함수는 재사용할 수 없는 문제가 있다.
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = selectBestCustomers(customers);
var biggestPurchases = getBiggestPurchases(bestCustomers);
return biggestPurchases;
}
function selectBestCustomers(customers) {
return filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
}
function getBiggestPurchases(customers) {
return map(customers, getBiggestPurchase);
}
function getBiggestPurchase(customer) {
return maxKey(customer.purchases, { total: 0 }, function(purchase) {
return purchase.total;
});
}
# (2) 콜백에 이름 붙이기
- 콜백을 빼내고 이름을 붙여 재사용할 수 있는 함수로 만든다.
- 호출 그래프의 아래쪽에 위치하므로 재사용하기 좋은 코드라는 것을 알 수 있다.
- 그리고 직관적으로 더 재사용하기 좋은 코드처럼 생겼다.
- 일반적으로 이 방법이 더 명확하다.
- 고차 함수를 그대로 쓰는 첫 번째 방법보다 이름을 붙인 두 번째 방법이 재사용하기도 더 좋다.
- 인라인 대신 이름을 붙여 콜백을 사용하면 단계가 중첩되는 것도 막을 수 있다.
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 mapKey(customers.purchases, { total: 0 }, getPurchaseToal);
}
function getPurchaseToal(purchase) {
return purchase.total;
}
# 3. 스트림 결합(stream fusion)
# (1) 스트림 결합 개념
map()
,filter()
,reduce()
체인을 최적화하는 것을 스트림 결합이라고 한다.filter()
와map()
은 모두 새로운 배열을 만드는데 함수가 호출될 때마다 새로운 배열이 생겨 크기가 클 수도 있고 비효율적이라고 생각할 수 있다.- 하지만 대부분 문제가 되지 않는다.
- 만들어진 배열이 배열이 필요 없을 때 가비지 컬렉터가 빠르게 처리하기 때문이고 현대 가비지 컬렉터를 매우 빠르다.
- 이 방법의 제일 큰 목적은 '최적화'이다.
- 병목이 생겼을 때만 쓰는 것이 좋고 대부분의 경우에는 여러 단계를 사용하는 것이 더 명확하고 읽기 쉽다.
# (2) 스트림 결합 적용
map()
// before
// 값 하나에 map() 두 번 사용
var names = map(customers, getFullName);
var nameLengths = map(names, stringLength);
// after
// map()을 한 번 사용해도 같다.
// 이 코드가 가비지 컬렉션이 필요 없다.
var nameLengths = map(customers, function(customers) {
return stringLength(getFullName(customer));
});
filter()
// before
// 값 하나에 filter() 두 번 사용
var goodCustomers = filter(customers, isGoodCustomer);
var withAddresses = filter(goodCustomers, hasAddress);
// after
// filter()을 한 번 사용해도 같다.
// 이 코드가 가비지 컬렉션을 적게 한다.
var withAddresses = filter(customers, function(customer) {
return isGoodCustomer(customer) && hasAddress(customer);
});
reduce()
// before
// map() 다음에 reduce()를 사용
var purchaseTotals = map(purchases, getPurchaseTotal);
var purchaseSum = reduce(purchaseTotals, 0, plus);
// after
// reduce()를 한 번 사용해도 같다.
// map()을 사용하지 않았기 때문에 가비지 컬렉션할 중간 배열을 만들지 않았다.
var purchaseSum = reduce(purchases, 0, function(total, purchase) {
return total + getPurchaseTotal(purchase);
});
# 4. 반복문을 함수형 도구로 리팩터링하기
리팩터링할 코드
var answer = []; var window = 5; for (var i = 0; i < array.length; i += 1) { var sum = 0; var count = 0; for (var w = 0; w < window; w += 1) { var idx = i + w; if (idx < array.length) { sum += array[idx]; count += 1; } } answer.push(sum / count); }
# (1) 데이터 만들기
- 어떤 값에
map()
,filter()
를 단계적으로 사용하면 중간에 배열이 생기고 없어지는데for
반복문을 사용할 때는 처리할 모든 값이 배열이 들어있지 않아도 된다. - 첫 번째 팁은 데이터를 배열에 넣으면 함수형 도구를 쓸 수 있다.
- 위 코드에서 안쪽에 있는 반복문을 개선해보자.
for (var i = 0; i < array.length; i += 1) {
var sum = 0;
var count = 0;
var subArray = array.slice(i, i + window);
for (var w = 0; w < subArray.length; w += 1) {
sum += subArray[w];
count += 1;
}
answer.push(sum / count);
}
# (2) 한 번에 전체 배열을 조작하기
- 하위 배열을 만들었기 때문에 일부 배열이 아닌 배열 전체를 반복할 수 있다.
map()
,filter()
,reduce()
는 배열 전체에 동작하기 때문에 이제 이런 함수형 도구를 쓸 수 있다.
for (var i = 0; i < array.length; i += 1) {
var subArray = array.slice(i, i + window);
answer.push(average(subArray));
}
# (3) 작은 단계로 나누기
- 하지만 현재까지의 코드를 보면 배열의 항목을 도는 것이 아닌 인덱스를 가지고 반복해야 하는 문제가 있다.
- 인덱스로 반복하는 코드를 한 단계로 만들기 어렵거나 어쩌면 불가능할 수도 있다.
- 그래서 더 작은 단계로 나눠야 한다.
var indices = [];
for (var i = 0; i < array.length; i += 1) {
indices.push(i);
}
var window = 5;
var answer = map(indices, function(i) {
var subArray = array.slice(i, i + window);
answer.push(average(subArray));
});
map()
콜백 안에서 두 가지 일을 하고 있어 두 단계로 나눈다.
var indices = [];
for (var i = 0; i < array.length; i += 1) {
indices.push(i);
}
var window = 5;
var windows = map(indices, function(i) {
return array.slice(i, i + window);
});
var answer = map(windows, average);
- 마지막으로 남은 건 인덱스 배열을 만드는 코드를 빼내 유용한 함수로 정의하는 일이다.
// 재사용 가능한 추가 도구 생성
function range(start, end) {
var ret = [];
for (var i = start; i < end; i += 1) {
ret.push(i);
}
return ret;
}
var window = 5;
var indices = range(0, array.length); // 단계1. 인덱스 배열 생성
var windows = map(indices, function(i) { // 단계2. 하위 배열 만들기
return array.slice(i, i + window);
});
var answer = map(windows, average); // 단계3. 평균 계산하기
# 📌 체이닝 팁 요약
# 📍 데이터 만들기
- 함수형 도구는 배열 전체를 다룰 때 잘 동작한다.
- 배열 일부에 대해 동작하는 반복문이 있다면 배열 일부를 새로운 배열로 나눌 수 있다.
- 그리고
map()
,filter()
,reduce()
같은 함수형 도구를 사용하면 작업을 줄일 수 있다.
# 📍 배열 전체를 다루기
- 반복문 대신에 전체 배열을 한 번에 처리할 수 있을지 생각해보자.
map()
은 모든 항목을 변환하고filter()
는 항목을 없애거나 유지한다.- 그리고
reduce()
는 항목을 하나로 합친다. - 과감하게 배열 전체를 처리해보자.
# 📍 작은 단계로 나누기
- 알고리즘이 한 번에 너무 많은 일을 한다고 생각된다면 직관에 반하지만 두 개 이상의 단계로 나눠보자.
# 📍 [추가] 조건문을 filter()
로 바꾸기
- 반복문 안에 있는 조건문은 항목을 건너뛰기 위해 사용하는 경우가 있다.
- 앞 단계에서
filter()
를 사용해 거를 수 있다.
# 📍 [추가] 유용한 함수로 추출하기
map()
,filter()
,reduce()
는 함수형 도구의 전부가 아니다.- 자주 사용하는 함수형 도구일 뿐 얼마든지 더 추가할 수 있다.
# 📍 [추가] 개선을 위해 실험하기
- 좋은 방법을 찾기 위해 함수형 도구를 새로운 방법으로 다양하게 조합하며 시도하고 연습해보자.
# 📌 체이닝 디버깅 팁
# 📍 구체적인 것을 유지하기
- 파이프라인 단계가 많으면 잊어버리기 쉬우므로 각 단계에서 어떤 것을 하고 있는지 알기 쉽게 이름을 잘 지어야 한다.
# 📍 출력해보기
- 각 단계 사이에
console.log
혹은print
문으로 코드를 돌려 예상한 대로 동작하는지 출력하며 확인해보자
# 📍 타입을 따라가 보기
- 타입스크립트와 같은 정적 타입 언어를 사용하면 IDE에서 콜백 함수의 인자나 리턴 값이 어떤 타입인지 자동 추론해서 보여준다.
- 이 기능을 잘 활용하면 원하는 결과가 잘 도출되는지 확인하는데 유용하다.
# 5. reduce()
우아하게 사용하기
reduce()
를 이용하여 새로운 값을 만들 수 있다.
- 로그 데이터를 이용하여 새로운 장바구니 데이터 만들기
var itemsAdded = ['shirt', 'shoes', 'shirt', 'socks', 'hat'];
var shoppingCart = reduce(itemsAdded, {}, addOne);
function addOne(cart, item) {
if (!cart[item]) {
return add_item(cart, { name: item, quantity: 1, price: priceLookup(item) });
} else {
var quantity = cart[item].quantity;
return setFieldByName(cart, item, 'quantity', quantity + 1);
}
}
- 만약 위 상황에서 제품을 추가하거나 삭제하는 것을 모두 지원하려면?
- 아래와 같이 고객이 제품을 추가했는지 삭제했는지 알려주는 값과 제품에 대한 값을 함께 기록하면 고객이 제품을 삭제한 경우도 처리할 수 있다.
var itemOps = [['add', 'shirt'], ['add', 'shoes'], ['remove', 'shirt'], ['add', 'socks'], ['add', 'hat']];
var shoppingCart = reduce (itemOps, {}, function(cart, itemOp) {
var op = itemOp[0];
var item = itemOp[1];
if (op === 'add') {
return addOne(cart, item);
}
if (op === 'remove') {
return removeOne(cart, item);
}
});
function removeOne(cart, item) {
if (!cart[item]) {
return cart;
}
var quantity = cart[item].quantity;
if (quantity === 1) {
return remove_item_by_name(cart, item);
} else {
setFieldByName(cart, item, 'quantity', quantity - 1);
}
}
- 위 코드에서 중요한 기술은 인자를 데이터로 표현했다는 점이다.
- 배열에 동작 이름과 제품 이름인 인자를 넣어 동작을 완전한 데이터로 표현했다.
- 인자를 데이터로 만들면 함수형 도구를 체이닝하기 좋다.
- 체이닝을 할 때 리턴할 데이터를 다음 단계의 인자처럼 쓸 수 있도록 만들어보자.