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

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

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

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]

정리

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