Skip to main content

13장 함수형 도구 체이닝

written by
yongsk0066
yongsk0066 🏆Front End Engineer

이번 장에서 살펴볼 내용

  • 함수형 도구를 조합하는 법을 배운다.
  • 복잡한 반복문을 함수형 도구 체인으로 바꾸는 법을 배운다.
  • 데이터 변환 파이프라인을 만들어 작업을 수행하는 법을 배운다.

함수형 도구 체이닝

우수고객의 가장 비싼 주문을 찾는 함수

const biggestPurchasesBestCustomers = (customers: Customer[]) => {
// 1 단계
const bestCustomers: Customer[] = filter(
customers,
(customer) => customer.purchases > 3
);

// 2 단계
const biggestPurchases: Purchases[] = map(bestCustomers, (customer) => {
return reduce(
customer.purchases,
(biggest, purchase) => {
if (biggest.total > purchase.total) {
return biggest;
}
return purchase;
},
{ total: 0 }
);
});

return biggestPurchases;
};

콜백 분리

maxKey(customer.purchases, (purchase) => purchase.total, { total: 0 });

const maxKey = <T, K>(
array: T[],
keySelector: (item: T) => K,
defaultValue: K
): K => {
return reduce(
array,
(biggest, item) => {
if (keySelector(biggest) > keySelector(item)) {
return biggest;
}
return item;
},
defaultValue
);
};
const biggestPurchasesBestCustomers = (customers: Customer[]) => {
// 1 단계
const bestCustomers: Customer[] = filter(
customers,
(customer) => customer.purchases > 3
);

// 2 단계
const biggestPurchases: Purchases[] = map(bestCustomers, (customer) => {
return maxKey(customer.purchases, (purchase) => purchase.total, {
total: 0,
});
});

return biggestPurchases;
};

단계에 이름 붙이기 (방법 1)

const biggestPurchasesBestCustomers = (customers: Customer[]) => {
const bestCustomers: Customer[] = selectBestCustomers(customers);
const biggestPurchases: Purchases[] = getBiggestPurchases(bestCustomers);
return biggestPurchases;
};

const selectBestCustomers = (customers: Customer[]) => {
return filter(customers, (customer) => customer.purchases.length >= 3);
};

const getBiggerPurchases = (customers: Customer[]) => {
return map(customers, getBiggestPurchase);
};

const getBiggestPurchase = (customer: Customer) => {
return maxKey(customer.purchases, (purchase) => purchase.total, {
total: 0,
});
};

콜백에 이름 붙이기 (방법 2)

const biggestPurchasesBestCustomers = (customers: Customer[]) => {
const bestCustomers: Customer[] = filter(customers, isGoodCustomer);
const biggestPurchases: Purchases[] = map(bestCustomers, getBiggestPurchase);
return biggestPurchases;
};

const isGoodCustomer = (customer: Customer) => customer.purchases.length >= 3;

const getBiggestPurchase = (customer: Customer) => {
return maxKey(customer.purchases, getPurchaseTotal, { total: 0 });
};

const getPurchaseTotal = (purchase: Purchase) => purchase.total;

두 방법을 비교

  • 두번째 방법이 더 명확하다. (재사용성, 단계 중첩 방지)

효율성

  • filter(), map()은 매번 새 배열을 만든다
  • 비효율적이라고 생각 할 수 있으나 GC가 빠르게 처리하게 때문에 걱정 No!
  • 체인을 최적화 하는 것을 스트림 결합이라고 한다.
// 하나의 값에 map() 두번 사용
const names = map(customers, getFullName);
const nameLengths = map(names, StringLength);

// map() 한번 사용
const nameLengths = map(customers, (customer) =>
StringLength(getFullName(customer))
);

한 번 사용하는 곳은 GC가 필요 없다.

반복문 함수형도구로 리팩토링

  • 이해하고 다시 만들기
  • 단서찾기
// 결과의 배열
const answer = [];
const window = 5;

// 배열 개수만큼 반복
for (let i = 0; i < array.length; i++) {
const sum = 0;
const count = 0;

// 0~5작은 구간 반복
for (let w = 0; w < window; w++) {
const idx = i + w;
if (idx < array.length) {
sum += array[idx];
// 값의 누적
count += 1;
}
}
// 배열의 추가
answer.push(sum / count);
}

팁 1 데이터 만들기

const answer = [];
const window = 5;

for (let i = 0; i < array.length; i++) {
const sum = 0;
const count = 0;

// 데이터를 배열에 넣어 함수형 도구 쓸수 있게 만들기
const subArray = array.slice(i, i + window);
for (let w = 0; w < subArray.length; w++) {
sum += subArray[w];
count += 1;
}
answer.push(sum / count);
}

팁 2 전체 배열 한번에 조작하기

const answer = [];
const window = 5;

for (let i = 0; i < array.length; i++) {
// 하위 배열을 만들기 위해 반복문의 인덱스를 사용
const subArray = array.slice(i, i + window);
answer.push(average(subArray));
}

팁 3 작은 단계로 나누기

const indices = [];

for (let i = 0; i < array.length; i++) {
indices.push(i);
}

const window = 5;
const answer = map(indices, (i) => {
const subArray = array.slice(i, i + window);
return average(subArray);
});
const indices = [];

for (let i = 0; i < array.length; i++) {
indices.push(i);
}

const window = 5;
const windows = map(indices, (i) => array.slice(i, i + window));

const answer = map(windows, average);
const range = (start: number, end: number) => {
const result = [];
for (let i = start; i < end; i++) {
result.push(i);
}
return result;
};

const window = 5;

const indices = range(0, array.length);
const windows = map(indices, (i) => array.slice(i, i + window));
const answer = map(windows, average);

절차적 코드와 함수형 코드 비교

const answer = [];
const window = 5;

for (let i = 0; i < array.length; i++) {
const sum = 0;
const count = 0;

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

const indices = range(0, array.length);
const windows = map(indices, (i) => array.slice(i, i + window));

const answer = map(windows, average);

// 유틸 함수
const range = (start: number, end: number) => {
const result = [];
for (let i = start; i < end; i++) {
result.push(i);
}
return result;
};

체이닝 팁 요약

  • 데이터 만들기
  • 배열 전체를 다루기
  • 작은 단계로 나누기
  • 조건문을 filter 로 바꾸기
  • 유용한 함수 추출하기
  • 개선을 위해 실험하기(노력)

체이닝 디버깅을 위한 팁

  • 구체적인 것을 유지
  • 출력해보기
  • 타입 따라가기

다양한 함수형 도구

pluck()

const pluck = (array, field) => {
return map(array, (obj) => obj[field]);
};

const prices = pluck(products, 'price');

concat()

const concat = (arrays) => {
const ret = [];
forEach(arrays, (array) => {
forEach(array, (item) => {
ret.push(item);
});
});
return ret;
};

const purchaseArrays = pluck(customers, 'purchases');
const allPurchases = concat(purchaseArrays);

frequenciesBy(), groupBy()

const frequenciesBy = (array, f) => {
const ret = {};
forEach(array, (item) => {
const key = f(item);
if (ret[key]) ret[key] += 1;
else ret[key] = 1;
});
return ret;
};

const groupBy = (array, f) => {
const ret = {};
forEach(array, (item) => {
const key = f(item);
if (ret[key]) ret[key].push(item);
else ret[key] = [item];
});
return ret;
};

값을 만들기 위한 reduce()

const itemsAdded = ['shirt', 'shoes', 'socks', 'hat'];

const shippingCart = reduce(
itemsAdded,
(cart, item) => {
if (!cart[item]) {
return addItem(cart, {
name: item,
quantity: 1,
price: priceLookup(item),
});
} else {
const quantity = cart[item].quantity;
return setFieldByName(cart, item, 'quantity', quantity + 1);
}
},
{}
);
const shippingCart = reduce(itemsAdded, addOne, {});

const addOne = (cart, item) => {
if (!cart[item]) {
return addItem(cart, {
name: item,
quantity: 1,
price: priceLookup(item),
});
} else {
const quantity = cart[item].quantity;
return setFieldByName(cart, item, 'quantity', quantity + 1);
}
};

데이터를 사용해 창의적으로 만들기

const itemOps = [
['add', 'shirt'],
['add', 'shoes'],
['remove', 'socks'],
['remove', 'hat'],
];

const shippingCart = reduce(
itemOps,
(cart, itemOp) => {
const op = itemOp[0];
const item = itemOp[1];

if (op === 'add') return addOne(cart, item);
if (op === 'remove') return removeOne(cart, item);
},
{}
);