Array.reduce 자바스크립트 사용 방법

원본: https://www.freecodecamp.org/news/reduce-f47a7da511a9/

자바스크립트의 reduce 메소드는 함수형 프로그래밍의 기본 중 하나입니다.

어떻게 작동하는지, 언제 사용해야 하는지, 어떤 일을 할 수 있는지 하나하나 살펴보겠습니다.

reduce 메소드 기본

이 메소드로는 돈의 액수가 나열된 배열이 있고 그 금액을 모두 더하고 싶을 때 사용할 수 있습니다.

const euros = [29.76, 41.85, 46.5];

euros.reduce((total, amount) => total + amount);
reduce 예제

개발자 도구에서 코드를 실행하면 위의 화면과 같은 결과를 얻을 수 있습니다.

사용 방법:

  • 이번 예제에서는 total 이라는 변수와 amount를 전달받아 사용했습니다.
  • 이 메소드는 for 루프와 마찬가지로 배열의 모든 요소에 루프를 돕니다.
  • 루프가 시작되면 total 값은 배열의 첫 번째 요소인 29.76이 되고 amount는 그 다음 요소인 41.85가 됩니다.
  • 이 예제에서는 모든 amount를 total에 더하려고 합니다.
  • 매 루프 마다 배열의 요소에 대해 계산이 이루어지며 다음번 루프에는 오른쪽 요소에 접근합니다.
  • 배열에 더 이상 요소가 남아있지 않으면 이 메소드는 total 값을 리턴합니다.

ES5 버전의 reduce 메소드

ES6 구문에 익숙하지 않다면 다음과 같이 사용할 수도 있습니다:

var euros = [29.76, 41.85, 46.5]; 

euros.reduce(function(total, amount) {
  return total + amount;
});
reduce es5 예제

위의 예제에서는 const 대신 var를 사용했습니다. 그리고 화살표 함수를 쓰지 않고 function 이라는 키워드를 사용했고 명시적으로 return 했습니다.

이 후의 예제는 ES6 구문을 사용하겠습니다. ES6 구문이 더 간결하고 오류가 발생할 여지가 적기 때문입니다.

reduce 메소드로 평균 계산하기

숫자 목록의 총 합을 리턴하기 전에 이 합계를 배열의 길이로 나눌 수 있습니다.

이를 수행하는 방법은 이 메소드의 또 다른 인수를 활용하는 것 입니다.

이 새로운 인수 중 첫 번째는 index 입니다. for 루프와 마찬가지로 index는 배열에 루프도는 횟수를 나타냅니다.

두 번째는 배열 차체를 나타냅니다. 다음 예제를 보겠습니다:

const euros = [29.76, 41.85, 46.5];

euros.reduce((total, amount, index, array) => {
  total += amount;
  if (index === array.length-1) { 
    return total/array.length;
  } else {
    return total;
  }
});
평균 계산 예제

reduce 메소드로 map, filter 구현

위의 예제를 완전히 이해했다면 이제 이를 원하는 방식으로 사용할 수 있습니다.

예를 들어, 값을 더하기 전에 값을 두 배로 만들거나 반으로 만들어 계산할 수도 있고 10 보다 큰 숫자만 더할 수도 있습니다.

이와 같이 reduce 메소드를 사용하면 개발자가 원하는 로직을 배열의 모든 요소를 돌며 실행하도록 할 수 있습니다.

그리고 항상 단일 값을 리턴할 필요도 없이 배열을 새 배열로 변환할 수 있습니다.

예를 들면 배열의 모든 요소를 두배로한 다른 배열을 만들 수 있습니다. 이렇게 하려면 accumulator 값을 빈 배열로 초기화 해야합니다.

초기 값은 리듀서가 시작할 때 사용할 값입니다. 다음의 예제에서 보는 바와 같이 맨 뒤에 0이 추가되었습니다:

const average = euros.reduce((total, amount, index, array) => {
  total += amount;
  return total/array.length;
}, 0);

이전 예제에서는 초기 값을 생략했습니다. 초기 값을 생략하면 total은 기본적으로 배열의 첫 번째 요소가 됩니다.

초기 값을 빈 배열로 설정하면 각각의 amount를 total에 넣을 수 있습니다. 숫자 배열의 모든 값이 두 배가 되는 다른 배열로 리듀스하고 싶으면 amount * 2를 total에 push 하면 됩니다.

const euros = [29.76, 41.85, 46.5];

euros.reduce((total, amount) => {
  total.push(amount * 2);
  return total;
}, []);
배열 요소 모두에 2 곱하기

위의 예제 코드로 모든 amount를 두 배로 가지는 새로운 배열을 만들었습니다. 이는 map 메소드의 동작과 동일합니다.

또한 리듀서 내부에 if 문을 추가하여 두 배로 늘리고 싶지 않은 숫자를 필터링 할 수도 있습니다. 이는 filter 메소드의 동작과 동일하게 작동합니다.

const euro = [29.76, 41.85, 46.5];

euro.reduce((total, amount) => {
  if (amount > 30) {
    total.push(amount);
  }
  return total;
}, []);

하지만 위와 같은 상황에서는 map 메소드나 filter 메소드를 사용하는 것이 더 간단합니다.

reduce 메소드는 map, filter를 동시에 수행해야하고 검토해야 할 데이터가 많을 때 사용하는게 이점이 있습니다.

map, filter를 체이닝하여 사용하는 것은 루프를 두 번 도는 것이므로 reduce 메소드를 사용하면 단일 패스로 필터링과 맵핑을 수행할 수 있습니다.

reduce로 자료 집계하기

항목에 대한 배열을 가지고 있고 각각의 항목이 몇 개씩 존재하는 알고싶은 경우 자료를 집계할 수 있습니다.

const fruitBasket = ['banana', 'cherry', 'orange', 'apple', 'cherry', 'orange', 'apple', 'banana', 'cherry', 'orange', 'fig' ];

fruitBasket.reduce((tally, fruit) => {
  tally[fruit] = (tally[fruit] || 0) + 1;
  return tally;
} , {});

배열의 항목을 집계하려면 초기 값은 앞선 예제와는 다르게 빈 배열이 아닌 빈 객체여야 합니다.

객체로 초기 값을 설정했으므로 이제 key/value 쌍을 저장할 수 있습니다.

저장된 key/value 쌍을 검사하여 이미 키가 존재하는 경우 1 씩 증가시켜 항목의 갯수를 집계할 수 있습니다.

배열의 배열 평면화

중첩된 배열의 배열(2 차원 배열)을 1 차원 배열로 평면화할 수 있습니다.

다음의 예제 처럼 초기 값을 빈 배열로 설정한 다음 concat 메소드를 사용하여 amount를 total에 연결하면 됩니다:

const data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

data.reduce((total, amount) => {
  return total.concat(amount);
}, []);
배열 평면화 예제

데이터가 위의 예제 보다 더 복잡한 경우도 있을 것 입니다.

예를 들어 아래 data 처럼 색상 정보를 가지는 객체의 배열이 있다고 가정해 보겠습니다:

const data = [
  {a: 'happy', b: 'robin', c: ['blue','green']}, 
  {a: 'tired', b: 'panther', c: ['green','black','orange','blue']}, 
  {a: 'sad', b: 'goldfish', c: ['green','red']}
];

여기에서 색상 값 만을 뽑아내려면 각각의 객체의 amount.c에 접근하여 forEach 루프 메소드를 활용해 모든 항목을 total에 push 하면 됩니다:

const colors = data.reduce((total, amount) => {
  amount.c.forEach(color => {
    total.push(color);
  })
  return total;
}, []);

여기서 중복된 색상을 제거하고 싶다면 해당 값이 이미 존재하는지 한번더 검사하면 됩니다:

const uniqueColors = data.reduce((total, amount) => {
  amount.c.forEach(color => {
    if (total.indexOf(color) === -1) {
      total.push(color);
    }
  });
  return total;
}, []);

피해야 할 실수

초기 값을 지정하지 않으면 reduce 메소드는 배열의 첫 번째 항목을 초기 값으로 지정합니다.

앞선 예제에서 숫자 목록의 총 합을 구했을 때는 초기 값을 생략해도 잘 작동했지만, 과일의 갯수를 파악한다거나 하는 경우 초기 값을 생략하면 잘못된 결과를 리턴할 것 입니다.

이는 저지르기 쉬운 실수이며 디버깅 시 가장 먼저 확인해 봐야할 사항입니다.

또 다른 실수로는 total을 리턴하지 않는 것 입니다. 리듀서가 작동하려면 무언가를 리턴해야 합니다. 실제로 원하는 값을 리턴하는지 다시 확인해봐야 합니다.

관련 글