자바스크립트
함수형 프로그래밍
reduce()

자바스크립트 reduce() 함수 사용법: 배열 index, 초기값, 객체, 중복제거, 조건문, async, break

자바스크립트의 Array.prototype.reduce() 메소드는 가장 자주 사용되는 배열 (array) 메서드 중 하나이며, 자바스크립트의 함수형 프로그래밍적인 면모를 보여주는 대표적인 예입니다.

프로그래밍을 처음 배우면 절차적, 객체지향 프로그래밍부터 접하기 때문에, 함수형 메소드 그 중에서도 reduce() 메소드를 직관적으로 이해하기 쉽지 않습니다. 그래서 이번 포스팅에서는 reduce() 메소드`를 이해하기 위해 함수형 프로그래밍부터 다양한 실제 활용 예제까지 모두 살펴보려고 합니다.

1. 간단히 설명하는 자바스크립트 함수형 프로그래밍

함수형 프로그래밍(Functional programming) 은 프로그래밍 패러다임 중 하나로, 프로그램 동작을 수학적인 의미의 함수를 기반으로 설계하는 기법을 말합니다.

함수형 프로그래밍에서는, 함수에 같은 값을 입력하는 한, 언제나 같은 결과값이 나와야 합니다. 이 원칙이 지켜지는 함수를 순수 함수(Pure function) 라고 하며, 함수의 동작에 있어서 어떠한 상태값도 변화시키지 않아야 합니다. 만약, 어떠한 변화가 생기면, 이를 Side effect 라고 하며, 함수형 프로그래밍은 이를 최소하하는 것을 지향합니다.

또한, 상태나 데이터의 불변성(Immutability) 을 강조하기 때문에, 자료 구조의 값을 수정하지 않는 대신, 새로운 자료 구조를 생성하는 방향으로 프로그램을 설계합니다. 배열 [1, 2, 3][1, 2, 4] 로 수정하고 싶으면 새로운 배열 [1, 2, 4] 를 만든다는 것입니다. 이렇게 설계한 프로그램은 데이터에 동시에 접근하는 동시성 프로그래밍(Concurrency) 등에 강력한 이점을 갖습니다.

마지막으로 자바스크립트는 함수를 1급 시민 (First-class citizens), 즉 변수, 파라미터, 리턴값 등 어디에서나 사용할 수 있는 객체로 대하기 때문에, 함수형 프로그래밍에 사용하기 우용한 언어입니다. 덕분에, 많은 JS 개발자들이 함수형 프로그래밍 기반으로 유지 관리가 쉽고 신뢰성 있는 코드를 작성합니다.

함수형으로 정의한 함수는 아래와 같습니다.

const add = (a, b) => a + b;
 
const result = add(1, 2); // 결과값은 언제나 3

1.1. 함수형 프로그래밍에서 reduce() 메소드 가 중요한 이유

reduce() 메소드는 기존 배열을 수정하지 않고 배열에 대한 연산을 수행하므로 자바스크립트 함수형 프로그래밍의 원칙을 지킵니다. 특히 배열을 단일 값으로 줄이거나 배열의 요소를 기반으로 결과를 누적할 때, 함수 밖에서 따로 누적값을 관리하지 않아도 되기 때문에. side effect에서도 자유롭습니다. 덕분에, 더 깔끔하고 유지 관리가 쉬운 코드를 작성하는 데 필수적인 메서드입니다.

2. reduce() 메소드 이해하기: 초기값, 인덱스, 누산기

reduce() 메소드는 배열을 순회하며 배열의 각 요소에 대해 제공된 함수(reducer)를 실행하여 단일 출력 값을 생성하는 배열 메서드입니다.

기본적인 문법은 다음과 같습니다:

array.reduce(reducerFunction, initialValue);
 
// reducerFunction은 아래와 같이, 정의할 수 있습니다.
 
array.reduce(((accumulator, currentValue, currentIndex, array) => returnValue), initialValue);

initialValue 는 초기값입니다. 이 값에 reducerFunction의 리턴값들이 더해집니다. 숫자의 합계를 구한다면 0, 최종 문자열을 구한다면 "" 등으로 사용합니다.

reducerFunction 은 총 4가지의 파라미터를 갖는 콜백 함수입니다.

  • 누산기 (accumulator) - 직전 인덱스까지의 모든 요소에 함수를 적용한 리턴값들을 누적한 값입니다
  • 현재값 (currentValue) - 현재 함수가 적용되는 요소의 값입니다
  • 인덱스 (currentIndex) - 현재 요소의 배열 인덱스입니다 (선택적)
  • 배열 (array) - 배열 자체에 대한 참조입니다 (선택적)

간단한 예제로 각 파라미터가 어떤 값을 갖게 되는지 알아봅니다.

const numbers = [ 1, 2, 3 ]
 
numbers.reduce((accumulator, currentValue, currentIndex, array) => {
    console.log(`누산기: ${accumulator}, 현재값: ${currentValue}, 인덱스: ${currentIndex}, 배열: ${array} `)
    return accumulator + currentValue
}, 0);
 
// Output:
// 누산기: 0, 현재값: 1, 인덱스: 0, 배열: 1,2,3
// 누산기: 1, 현재값: 2, 인덱스: 1, 배열: 1,2,3
// 누산기: 3, 현재값: 3, 인덱스: 2, 배열: 1,2,3
// 6

인덱스가 0인 가장 첫번째 리듀서 함수 호출에서 누산기는 초기값 0을 가지고 있습니다. 리듀서 함수는 누산기 값에 현재값 1을 더한 후 리턴하며, 이 값은 새로운 누산기 값이 됩니다. 이 과정이 반복되어 reduce() 함수의 최종 결과값으로 최종 누산기값인 6이 반환됩니다.

즉, 위 예제의 동작 원리를 풀어서 작성하면 다음과 같습니다.

const numbers = [ 1, 2, 3 ]
const reducerFunction = element => element
 
const returnValue = reducerFunction(numbers[0]) + reducerFunction(numbers[1]) + reducerFunction(numbers[2])
 
console.log(returnValue)
// Output: 6

2.1. break, continue 키워드에 주의하기

reduce() 함수를 포함한 대부분의 배열 메서드는 순회 작업을 조작하는 break, continue 키워드를 지원하지 않습니다. 만약 continue 키워드처럼 현재 요소에 아무 조작을 하지 않고자 한다면, 다음과 같이 조건문을 활용해 누적값을 그대로 리턴하세요. 아래 예제에서는 3을 누적하지 않고 지나갔습니다.

const numbers = [ 1, 2, 3 ]
const reducerFunction = (accumulator, element) => {
    console.log(`누산기: ${accumulator}, 현재값: ${element}`)
    if (element === 3) {
        return accumulator
    }
    return accumulator + element
}
 
numbers.reduce(reducerFunction, 0)
// Output: 3

3. 객체 조작에 사용하는 방법

reduce() 메소드는 객체의 속성에 접근해서 여러가지 작업을 수행하는 경우에도 유용하게 사용합니다. 이 때, 직접 객체 속성에 접근할 수도, Object.entries()와 같은 헬퍼 메소드를 이용할 수도 있습니다.

하나씩 예제로 확인해보겠습니다.

const people = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 },
    { name: 'Charlie', age: 25 },
    { name: 'David', age: 30 }
];
 
const groupedByAge = people.reduce((accumulator, currentValue) => {
    const ageGroup = currentValue.age;
    accumulator[ageGroup] = accumulator[ageGroup] || [];
    accumulator[ageGroup].push(currentValue);
    return accumulator;
}, {});
 
console.log(groupedByAge);
// Output:
// {
//  '25': [
//      { name: 'Alice', age: 25 },
//      { name: 'Charlie', age: 25 }
//  ],
//  '30': [
//      { name: 'Bob', age: 30 },
//      { name: 'David', age: 30 }
//  ]
// }

위 예제는 객체의 age 속성에 접근해서, 나이를 기준으로 객체를 2가지 그룹으로 분류하는 작업을 수행했습니다.

const grades = {
    math: 90,
    science: 80,
    history: 85,
    english: 95
};
 
const totalScore = Object.entries(grades).reduce((accumulator, [subject, score]) => {
    return accumulator + score;
}, 0);
 
console.log(totalScore); // Output: 350

만약, 하나의 객체의 여러 값들을 조작해야 한다면, Objext.entries() 메서드를 사용할 수 있습니다. 이 메서드는 위와 같이 객체의 속성과 속성값을 하나의 배열처럼 조작하도록 합니다. 이를 통해, 객체에도 reduce() 함수를 사용할 수 있게 되며, [subject, score]처럼 구조 분해 할당(Destructuring assignment)을 이용해 간결한 코드 작성도 가능합니다.

4. 중복 제거 등 실제 활용 예제

JS filter() 함수 - 3. filter()를 활용한 중복 제거 섹션에서도 중복 제거 테크닉을 코드 예제로 살펴보았는데요. reduce() 함수로도 배열에서 중복 요소를 제거하는 코드를 간단히 작성할 수 있습니다.

코드부터 확인해보겠습니다.

const array = [1, 2, 3, 2, 4, 3, 5, 1];
 
const uniqueArray = array.reduce((accumulator, element) => {
  if (!accumulator.includes(element)) {
    accumulator.push(element);
  }
  return accumulator;
}, []);
 
console.log(uniqueArray);
// Output: [1, 2, 3, 4, 5]

위 코드는 초기값을 빈 배열 []로 설정한 후, 현재 요소가 accumulator{js} 배열에 존재하지 않을 때 이 요소를 새로 추가합니다. 포함 여부 판별은 Array.includes() 메서드를 사용합니

JS filter() 함수 - 4. includes() 활용한 콜백 함수 정의하기 섹션에서 사용한 String.includes() 메서드와 비교해보세요.

reduce 함수가 반환하는 결과값 배열에는 중복되지 않는 요소들만 당겨 있는 것을 화인할 수 있습니다.

5. async 콜백 함수 사용법

배열 메서드에서 비동기 콜백 함수 사용하는 방법은 JS map() 메서드 - 6. Async callback: 비동기 콜백 함수 섹션에서 본 적이 있는데요. map() 메서드는 Promise.all() 메서드와 함께 비동기 + 동시성으로 동작하는 코드를 작성할 수 있었습니다.

하지만, reducer 메서드는 비동기 동작을 보조할 만한 수단이 없기 때문에, 비동기 콜백 함수와 사용할 수 없습니다. 이를 해결하기 위해서는, async await 키워드와 Promise 객체를 사용해서 비동기로 동작하는 사용자 정의 reduce 함수를 따로 정의해야 합니다.

예제 코드부터 살펴볼게요.

const array = [1, 2, 3, 4, 5];
 
const asyncReduce = async (array, callback, initialValue) => {
  let accumulator = initialValue;
 
  for (const element of array) {
    accumulator = await callback(accumulator, element);
  }
 
  return accumulator;
};
 
const sumAsync = async (a, b) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(a + b);
    }, 1000);
  });
};
 
const calculateSum = async () => {
  const sum = await asyncReduce(array, sumAsync, 0);
  console.log(sum);
};
 
calculateSum();
// Output: 15

예제의 asyncReduce() 함수는 for of 구문을 통해 배열을 순회하며, 비동기 콜백 함수를 호출하고 모든 작업을 왼료하도록 await합니다. 임의의 비동기 콜백 함수 sumAsync()는 1초를 기다린 후, 덧셈의 결과를 반환합니다. 마지막으로 calculateSum() 함수는 asyncReduce() 함수를 실제로 실행하는 메인 함수 역할을 합니다.

코드를 실행하면, 일정 시간이 지난 후, 모든 수의 총합인 15를 얻을 수 있습니다.

6. 마치며

자바스크립트 reduce() 메서드는 함수형 프로그래밍을 위한 강력하고 다재다능한 도구입니다. 이 메서드를 사용하면 불변성 및 단순성과 같은 원칙을 준수하면서, 배열에 대한 연산을 수행할 수 있습니다. 다른 배열 메서드와 함께 적절히 사용하며, 코드를 작성하면 직관적이고 가독성 높은 코드를 작성하시길 바랍니다.

copyright for Javascript reduce

© 2023 All rights reserved.