Skip to content

chp13. 함수형 도구 체이닝 ​

이전 장에서는 배열을 다루는 대표적 추상 함수인 map, filter, reduce를 살펴보았습니다. 이번 장에서는 단순 반복문 대신 여러 단계의 체인으로 복합 연산을 함수형으로 표현하는 방법을 다룹니다.

리팩토링 해보기 ​

jsx
function biggestPurchasesBestCustomers(customers) {
  // 단계 1. 3개 이상 구매한 고객 구하기
  var bestCustomers = filter(customers, function (customer) {
    return customer.purchases.length >= 3;
  });

  // 단계 2. 가장 비싼 구매 찾기
  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;
}

위 코드는

  1. 3회 이상 구매한 고객을 선별하고,
  2. 각 고객의 가장 비싼 구매 내역을 추출합니다.

요구사항은 충족하지만, 콜백이 중첩되어 읽기 어려운 문제가 있습니다. 아래 두 가지 방법으로 체인을 더 명확히 만들 수 있습니다.

첫 번째 방법: 단계에 이름 붙이기 ​

각 단계를 별도 함수로 분리하여 과정을 드러내는 이름을 부여합니다.

jsx
// 3회 이상 구매한 고객들의 '가장 비싼 구매' 목록을 반환합니다.
function biggestPurchasesBestCustomers(customers) {
  var bestCustomers = selectBestCustomers(customers);
  var biggestPurchases = getBiggestPurchases(bestCustomers);
  return biggestPurchases;
}

// purchases 개수가 3개 이상인 고객만 추립니다.
function selectBestCustomers(customers) {
  return filter(customers, function (customer) {
    return customer.purchases.length >= 3;
  });
}

// 각 고객을 해당 고객의 '가장 비싼 구매 1건'으로 매핑합니다.
function getBiggestPurchases(customers) {
  return map(customers, getBiggestPurchase);
}

// 고객의 구매 내역 중 total 값이 가장 큰 항목을 찾습니다.
function getBiggestPurchase(customer) {
  return maxKey(customer.purchases, { total: 0 }, function (purchase) {
    return purchase.total;
  });
}

단계가 분리되어 흐름이 선명해집니다. 다만 selectBestCustomers 내부의 인라인 콜백은 여전히 재사용이 어렵습니다.

두 번째 방법: 콜백에 이름 붙이기 ​

단계 자체가 아니라 단계에 쓰이는 콜백을 분리합니다. 이 방식이 일반적으로 더 재사용 가능하고 의도가 분명합니다.

jsx
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;
}

새로운 배열 생성을 최적화하기 ​

filter와 map은 각각 새 배열을 반환합니다. 성능이 우려될 수 있으나, 현대 가비지 컬렉터는 매우 효율적이므로 대부분의 경우 문제가 되지 않습니다. 다만 필요할 때는 단계를 합쳐 생성 횟수를 줄일 수 있습니다.

jsx
// 배열 2번 생성
const names = map(customers, getFullName);
const nameLengths = map(names, stringLength);

// 배열 1번 생성
const nameLengthsOptimized = map(customers, (customer) =>
  stringLength(getFullName(customer))
);

최적화는 명확한 병목이 확인될 때 적용하는 것이 바람직하며, 평소에는 단계의 가독성을 우선하는 편이 유지보수에 유리합니다.

반복문을 함수형 체인으로 리팩토링하는 팁 ​

1) 데이터 만들기 ​

배열을 새로 나누거나 변환하고 값을 누적할 때 map, filter, reduce를 사용합니다.

jsx
const nums = [1, 2, 3, 4, 5];
const squared = nums.map((n) => n * n); // [1, 4, 9, 16, 25]

2) 작은 단계로 나누기 ​

복잡한 로직을 여러 단계로 쪼개면 이해와 유지보수가 쉬워집니다.

jsx
const result = nums
  .filter((n) => n % 2 === 0) // [2, 4]
  .map((n) => n * n) // [4, 16]
  .reduce((acc, n) => acc + n); // 20

3) 조건문을 filter로 만들기 ​

반복문+조건문 조합보다 의도가 더 명확해집니다.

jsx
const even = nums.filter((n) => n % 2 === 0); // [2, 4]

4) 유용한 함수로 추출하기 ​

자주 쓰는 조건/변환을 함수로 빼면 이름만으로 의도가 드러납니다.

jsx
const isEven = (n) => n % 2 === 0;
const double = (n) => n * 2;

const doubledEven = nums.filter(isEven).map(double); // [4, 8]

정리 ​

함수형 도구 체이닝의 핵심은 복잡한 계산을 작은 단계로 분해하고, 그 단계를 명확히 드러내는 것입니다. 단계와 콜백에 의미 있는 이름을 부여하면 가독성과 재사용성이 향상됩니다. 배열 생성 최적화는 필요할 때만 적용하고, 평상시에는 읽기 쉬운 체인을 유지하는 것이 좋습니다.