Skip to content

함수형 도구 체이닝

반복문으로 "무엇을 할지"와 "어떻게 반복할지"가 섞여 있으면, 로직이 커질수록 가독성 · 재사용성 · 테스트 용이성이 급격히 나빠집니다.

이 글은 map/filter/reduce 같은 함수형 도구를 체인으로 엮어 복잡한 계산을 단계화하고, 필요할 때는 스트림 결합으로 최적화하는 실전 패턴을 정리합니다.

스터디 회차: 10회차 (2025년 9월 3일)

함수형 도구 체이닝이란

  • 체이닝은 map → filter → reduce처럼 단계를 순차적으로 연결해 최종 결과를 만드는 방식입니다.
  • 각 단계는 입력을 출력으로 변환하는 순수 함수이며, 중간 단계는 다음 단계의 입력이 됩니다.
  • 체이닝은 각 단계를 작은 순수 함수로 분리해 의도를 명확히 드러내고, 재사용성을 높이며, 단위 테스트 작성도 쉽게 만듭니다.

예제: 우수한 고객들의 가장 비싼 구매 찾기

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

문제점

  • 간결하지만 의도가 드러나지 않는 익명 콜백이 많아 읽기 어렵습니다.
  • 코드에 중첩된 리턴 구문은 어떤 일을 하는지 파악하기 어렵습니다.

개선하기 1: 단계에 이름 붙이기

tsx
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: 콜백에 이름 붙이기

tsx
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;
}
  • 콜백을 이름 있는 함수로 빼내 재사용성과 테스트 용이성을 높였습니다.
  • 중첩 감소로 로직 흐름이 평탄해집니다.

일반적으로는 두번째 방법이 더 명확합니다

  • 고차함수를 그대로 쓰는 첫번째 방법보다 이름을 붙인 두번째 방법이 읽기 쉽고 재사용하기도 더 좋습니다.
  • 지역적인 일회성 로직이면 첫번째 방법처럼 인라인도 괜찮습니다. 두 방법중 어떤 방법이 좋은지 비교해 결정합니다.

스트림 결합

  • 함수형 프로그래밍에서 여러 체인을 최적화하는 것을 스트림 결합이라고 합니다.
  • 여러 번의 순회를 한 번으로 합쳐 중간 배열 생성을 줄이는 최적화입니다.

map + map 결합

tsx
// 개선 전
var names = map(customers, getFullName);
var nameLengths = map(names, stringLength);

// 개선 후
var nameLengths = map(customers, function (customer) {
  return stringLength(getFullName(customer));
});
  • 결과는 동일하지만, 중간 배열을 만들지 않아 가비지 컬렉션 부담과 메모리 사용이 줄어듭니다.

filter + filter 결합(AND)

tsx
// 개선 전
var goodCustomers = filter(customers, isGoodCustomer);
var withAddresses = filter(goodCustomers, hasAddress);

// 개선 후
var withAddresses = filter(customers, function (customer) {
  return isGoodCustomer(customer) && hasAddress(customer);
});
  • 마찬가지로 중간 배열 (goodCustomers) 을 제거합니다.

reduce로 추가 계산 수용하기

tsx
// 개선 전
var purchaseTotals = map(purchases, getPurchaseTotal);
var purchaseSum = reduce(purchaseTotals, 0, plus);

// 개선 후
var purchaseSum = reduce(purchases, 0, function (total, purchase) {
  return total + getPurchaseTotal(purchase);
});
  • map을 사용하지 않았기 때문에 가비지 컬렉션할 중간 배열을 만들지 않았습니다.
  • reduce 콜백 안으로 추가 계산을 넣어 한 번의 순회로 끝냅니다.

언제 결합을 적용하면 좋을까요?

  • 스트림 결합은 최적화입니다. 병목이 보일 때만 적용하세요.
  • 대부분의 경우 여러 단계를 나눠 쓰는 편이 더 명확하고 읽기 쉽습니다.

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

기존에 있던 반복문을 함수형 도구로 리팩토링 해야하는 경우는 두가지 방법이 있습니다.

  1. 이해하고 다시 만들기: 어떤 일을 하는지 파악한 다음 다시 만드는 것입니다.
  2. 단서를 찾아 리팩토링: 반복문을 하나씩 선택한 다음 함수현 도구 체인으로 바꿉니다.

예제: 배열을 이동하며 평균 구하기

tsx
var answer = [];
var window = 5;
for (var i = 0; i < array.length; i++) {
  // map
  var sum = 0;
  var count = 0;
  for (var w = 0; w < window; w++) {
    // reduce
    var idx = i + w;
    if (idx < array.length) {
      sum += array[idx];
      count += 1;
    }
  }
  answer.push(sum / count);
}
  • 원래 배열 크기만큼 answer 배열에 항목을 추가하고 있는 부분은 map을 사용할 수 있습니다.
  • 안쪽 반복문에 배열을 돌면서 항목을 값 하나로 만들고 있는 부분은 reduce를 사용하기에 좋습니다.

개선하기 1: 데이터 만들기

tsx
var answer = [];
var window = 5;

for (var i = 0; i < array.length; i++) {
  var sum = 0;
  var count = 0;
  var subarray = array.slice(i, i + window);
  for (var w = 0; w < subarray.length; w++) {
    sum += subarray[w];
    count += 1;
  }
  answer.push(sum / count);
}
  • 안쪽 반복문은 어떤 범위로 배열을 반복합니다. 이 범위에 맞는 하위 배열을 만들게 되면 배열 전체를 반복할 수 있습니다. 이는 함수형 도구를 사용하기에 적합합니다.
  • 하위 배열 subarray 을 만들어줍니다.

개선하기 2: 한 번에 전체 배열을 조작하기

tsx
var answer = [];
var window = 5;

for (var i = 0; i < array.length; i++) {
  var subarray = array.slice(i, i + window);
  answer.push(average(subarray));
}
  • 목적이 평균이므로 average 를 사용하여 의도를 드러냅니다.
  • 남은 for문은 배열 전체를 반복하기에 map 을 사용하기 좋아보이지만, 반복문이 배열 항목을 사용하지 않으므로 map을 사용할 수 없습니다.

개선하기 3: 작은 단계로 나누기

  • 배열의 항목이 아니라 인덱스를 가지고 반복해야하는 문제가 있습니다.
  • 인덱스로 방문하는 코드를 한단계로 만들기 어렵다면 작은 단계로 나누어 해결합니다.
    1. 먼저, 이를 해결하기 위해 인덱스가 있는 배열을 만듭니다.
    2. 그리고 인덱스 배열에 함수형 도구를 사용해 줍니다.
tsx
var indices = [];

for (var i = 0; i < array.length; i++) indices.push(i);

var window = 5;
var answer = map(indices, function (i) {
  var subarray = array.slice(i, i + window); // 1. 하위 배열 만들기
  return average(subarray); // 2. 평균 계산하기
});
  • 인덱스 배열 indices 를 만듭니다.
  • 그리고 indices에 map을 적용합니다.
  • 추가로 map이 하위 배열을 만들고 평균을 계산하는, 두 가지 일을 하고 있으므로 단계를 나눌 수 있습니다.
tsx
// 인덱스 배열 만들기
var indices = [];

for (var i = 0; i < array.length; i++) indices.push(i);

var window = 5;

// 1. 하위 배열 만들기
var windows = map(indices, function (i) {
  return array.slice(i, i + window);
});

// 2. 평균 계산하기
var answer = map(windows, average);
  • 단계가 나뉘어 각 역할이 명확해졌습니다.
tsx
// 인덱스 배열 만들기
function range(start, end) {
  var ret = [];
  for (var i = start; i < end; i++) ret.push(i);
  return ret;
}

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);
  • 마지막으로 인덱스 배열을 만드는 코드를 빼내서 재사용 가능한 유틸함수로 정의합니다.

체이닝 팁 정리

  • 범위를 명시화해 함수형 도구를 사용하기
  • 배열 전체를 다루기
  • 각 단계는 하나의 일만 하도록 작은 단계로 나누기
  • 조건문을 filter로 바꾸기
  • 유용한 함수로 추출하여 재사용하기
  • 좋은 방법을 찾기 위해 많은 것을 시도하고 연습하기

정리하기

  • 함수형 체이닝은 반복문을 단계별로 분리해 더 읽기 좋고 유지보수하기 쉬운 코드를 만드는 방법입니다.
  • 콜백과 단계를 이름 있는 함수로 빼내면 의도가 드러나고 재사용성이 높아집니다.
  • 스트림 결합은 성능 최적화를 위해 쓸 수 있지만, 병목이 생겼을 때만 적용하는 것이 좋습니다.
  • 결국 중요한 것은 가독성과 성능 사이의 균형을 맞추는 것입니다.