Logo

자바스크립트 배열의 reduce() 사용법

자바스크립트의 배열은 여러가지 메서드를 제공하고 있지만 그 중에서 가장 강력한 녀석을 뽑으라면 단연 reduce()를 뽑을 수 있을텐데요. 워낙 범용적으로 쓰일 수 있는 메서드이다 보니 reduce()가 사용된 코드를 해석하는데 어려움을 느끼시는 분들이 많습니다.

이번 포스팅에서는 reduce() 메서드의 기본 사용법을 알아보고 다양한 예제를 통해 어떻게 실제 개발에서 활용할 수 있는지 배워보겠습니다.

기본 문법

어떤 배열을의 reduce() 메서드를 호출하면 배열을 상대로 각 요소인자로 넘어온 콜백 함수를 실행하여 누적된 하나의 결과값을 반환합니다.

array.reduce(<콜백 함수>, <초기값>);

콜백 함수에는 총 4개의 인자가 넘어오는데, 대부분의 경우에는 첫 2개의 인자만 필요합니다.

  • accumulator: 이전 요소를 상대로 콜백 함수를 실행한 결과 (누적자)
  • currentValue: 현재 요소의 값
  • currentIndex: 현재 요소의 인덱스
  • array: reduce() 메서드를 호출하는 배열

타입 스크립트를 사용할 때는 다음과 같은 형태로 최종 결과값의 타입을 지정해줄 수 있습니다. 타입을 지정해주지 않으면 초기값의 타입을 추론하여 결과 타입으로 사용합니다.

array.reduce<결과 타입>(<콜백 함수>, <초기값>);

누적 계산

reduce() 메서드의 가장 간단한 활용 사례는 배열을 상대로 누적 계산을 할 때입니다.

예를 들어, 배열에 들어있는 숫자의 누적합을 구해보겠습니다.

const numbers = [2, 4, 3, 1];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 10

reduce() 메서드 없이도 반복문을 사용해서 계산할 수 있지만 코드가 더 장황해집니다.

const numbers = [2, 4, 3, 1];
let sum = 0;
for (const num of numbers) {
  sum += num;
}
console.log(sum); // 10

뿐만 아니라, reduce() 메서드를 쓸 때는 sumconst 키워드로 선언하여 변경이 불가능하게 할 수 있어서 더 견고한 코드를 작성할 수 있습니다.

최소값, 최대값 계산

reduce() 메서드는 최소값이나 최대값을 구할 때도 사용할 수 있습니다.

예를 들어, 숫자 배열에서 최소값과 최대값을 계산해보겠습니다.

const numbers = [2, 4, 3, 1];
const min = numbers.reduce((min, num) => (min < num ? min : num));
console.log(min); // 1
const max = numbers.reduce((max, num) => (max > num ? max : num));
console.log(max); // 4

위 코드를 자세히 보시면 초기값을 생략했다는 것을 알 수 있는데요. 이렇게 초기값을 지정해주지 않으면, 배열의 첫 번째 값이 초기값으로 사용이 됩니다.

그래서 빈 배열을 상대로 사용하게 되면 아래와 같이 오류가 발생하게 됩니다.

const numbers = [];
const min = numbers.reduce((min, num) => (min < num ? min : num));
// TypeError: reduce of empty array with no initial value

이러한 문제를 방지하기 위해서 reduce() 메서드를 사용할 때는 꼭 초기값을 설정해주는 습관을 들이면 좋습니다.

const min = numbers.reduce(
  (min, num) => (min < num ? min : num),
  Number.MAX_VALUE // 초기값
);

const max = numbers.reduce(
  (max, num) => (max > num ? max : num),
  Number.MIN_VALUE // 초기값
);

개수 세기

reduce() 함수는 배열에서 각 원소의 개수를 셀 때도 많이 사용됩니다.

예를 들어서, 배열에 들어 있는 각 과일의 개수를 세보겠습니다.

const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const fruitCounts = fruits.reduce((counter, fruit) => {
  if (fruit in counter) {
    counter[fruit]++;
  } else {
    counter[fruit] = 1;
  }
  return counter;
}, {});
console.log(fruitCounts); // { apple: 3, banana: 2, orange: 1 }

ES6가 도입된 이후에는 전개 연산자를 선호하시는 개발자 분들이 많은 것 같습니다.

const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const fruitCounts = fruits.reduce(
  (counter, fruit) => ({
    ...counter,
    [fruit]: fruit in counter ? counter[fruit] + 1 : 1,
  }),
  {}
);
console.log(fruitCounts); // { apple: 3, banana: 2, orange: 1 }

타입스크립트

타입스크립트에서 reduce() 메서드를 사용할 때는 타입 오류가 발생하기 쉬워서 주의가 필요합니다.

예를 들어, 방금 작성한 동일한 코드를 타입스크립트로 컴파일하면 다음과 같은 타입 오류가 발생하는 것을 보실 수 있으실 거에요.

const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const fruitCounts = fruits.reduce((counter, fruit) => {
  counter[fruit] = fruit in counter ? counter[fruit] + 1 : 1;
  // ^? (parameter) counter: {}
  return counter;
}, {});
console.log(fruitCounts); // { apple: 3, banana: 2, orange: 1 }

초기값으로 빈 객체를 넘기기 때문에, 누적자인 counter의 타입이 빈 객체로 추론이 되기 때문입니다. 이러한 타입 오류를 해결하려면 reduce<결과 타입>의 형태로 결과 값의 타입을 명시적으로 표시해줘야 합니다.

const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const fruitCounts = fruits.reduce<{ [key: string]: number }>(
  (counter, fruit) => {
    counter[fruit] = fruit in counter ? counter[fruit] + 1 : 1;
    return counter;
  },
  {}
);
console.log(fruitCounts); // { apple: 3, banana: 2, orange: 1 }

타입스크립트의 유틸리티 타입인 Record를 활용하셔도 되겠죠?

const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const fruitCounts = fruits.reduce<Record<string, number>>((counter, fruit) => {
  counter[fruit] = fruit in counter ? counter[fruit] + 1 : 1;
  return counter;
}, {});
console.log(fruitCounts); // { apple: 3, banana: 2, orange: 1 }

배열 평탄화

2차원 배열을 1차원 배열로 평탄화할 때도 reduce() 메서드가 자주 쓰입니다.

const nested = [
  [1, 2],
  [3, 4],
  [5, 6],
];
const flattened = nested.reduce((nums, num) => nums.concat(num), []);
console.log(flattened); // [1, 2, 3, 4, 5, 6]

ES6가 도입된 이후에는 전개 연산자를 선호하시는 개발자 분들이 많은 것 같습니다.

const nested = [
  [1, 2],
  [3, 4],
  [5, 6],
];
const flattened = nested.reduce((nums, num) => [...nums, ...num], []);
console.log(flattened); // [1, 2, 3, 4, 5, 6]

자바스크립트에서 배열을 합치는 방법에 대해서는 별도 포스팅에서 자세히 다루고 있습니다.

속성 추출

reduce() 메서드는 객체 배열에서 특정 속성의 값만 추출하고 싶을 때도 사용할 수 있습니다.

예를 들어, 사용자 배열에서 국가 속성값 추출해서 유일한 국가 집합을 만들어보겠습니다. 중복값을 제거하기 위해서 세트(Set)를 사용하였습니다.

const users = [
  { name: "John", age: 25, country: "US" },
  { name: "Jane", age: 30, country: "KR" },
  { name: "Robin", age: 22, country: "CA" },
  { name: "Doe", age: 13, country: "US" },
  { name: "Smith", age: 20, country: "KR" },
];
const distinctCountries = users.reduce((countries, user) => {
  countries.add(user.country);
  return countries;
}, new Set());
console.log(distinctCountries); // { "US",  "KR", "CA" }

자바스크립트에서 고유한 값들의 집합을 다루는 자료구조인 세트(Set)에 대해서는 별도 포스팅에서 자세히 다루고 있습니다.

비슷한 방식으로 이번에는 나이 속성값을 추출하여 사용자의 최소 나이를 계산해보겠습니다.

const users = [
  { name: "John", age: 25, country: "US" },
  { name: "Jane", age: 30, country: "KR" },
  { name: "Robin", age: 22, country: "CA" },
  { name: "Doe", age: 13, country: "US" },
  { name: "Smith", age: 20, country: "KR" },
];
const minAge = users.reduce((min, user) => {
  const age = user.age;
  return age < min ? age : min;
}, Number.MAX_VALUE);
console.log(minAge); // 13

원소 분류

reduce() 메서드는 특히 데이터를 그룹화할 때 빛을 발합니다.

예를 들어, 사용자 배열을 국가를 기준으로 분류해보겠습니다.

const users = [
  { name: "John", age: 25, country: "US" },
  { name: "Jane", age: 30, country: "KR" },
  { name: "Robin", age: 22, country: "CA" },
  { name: "Doe", age: 13, country: "US" },
  { name: "Smith", age: 20, country: "KR" },
];
const usersByCountry = users.reduce((users, user) => {
  const country = user.country;
  if (!(country in users)) {
    users[country] = [];
  }
  users[country].push(user);
  return users;
}, {});
console.log(usersByCountry);

각 국가를 키로 해당 국적자 목록을 값으로 객체를 만들어졌습니다.

{
  US: [
    { name: "John", age: 25, country: "US" },
    { name: "Doe", age: 13, country: "US" }
  ],
  KR: [
    { name: "Jane", age: 30, country: "KR" },
    { name: "Smith", age: 20, country: "KR" }
  ],
  CA: [
    { name: "Robin", age: 22, country: "CA" }
  ],
}

타입스크립트를 사용할 때는 타입 오류가 발생하지 않도록 결과 타입을 명시해줘야 합니다.

const users = [
  { name: "John", age: 25, country: "US" },
  { name: "Jane", age: 30, country: "KR" },
  { name: "Robin", age: 22, country: "CA" },
  { name: "Doe", age: 13, country: "US" },
  { name: "Smith", age: 20, country: "KR" },
];
const usersByCountry = users.reduce<Record<string, typeof users>>(
  (users, user) => {
    const country = user.country;
    if (!(country in users)) {
      users[country] = [];
    }
    users[country].push(user);
    return users;
  },
  {}
);
console.log(usersByCountry);

자바스크립트에서 데이터를 그룹화하는 최신 방법인 groupBy()에 대해서는 별도 포스팅을 참고하세요.

마치면서

지금까지 자바스크립트 배열의 reduce() 메서드를 어떻게 사용하는지 다양한 예제를 통해서 살펴보았습니다.

reduce() 메서드는 매우 강력하지만 너무 많이 사용하면 코드가 상당히 복잡해보일 수 있다는 단점도 있습니다. 따라서 납용하지 않도록 쓰시기 전에 꼭 필요한 상황인지 생각해보시면 도움이 될 것 같습니다.