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, Array<(typeof users)[number]>>
>((users, user) => {
  const country = user.country;
  if (!(country in users)) {
    users[country] = [];
  }
  users[country].push(user);
  return users;
}, {});
console.log(usersByCountry);

마치면서

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

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