
reduce() 메서드는 자바스크립트의 배열 메서드로서, 제공하는 함수를 사용해 배열의 요소를 순회하며 값 하나로 만드는 축소 동작을 실행한다.
reduce() 메서드의 기본 문법은 다음과 같다.
const result = array.reduce(callback, initialValue);
위와 같이 reduce() 메서드는 축소 동작을 행하는 콜백 함수를 첫 번째 인자로 받고, 두 번째 인자로는 함수에 전달할 초깃값을 받는다.
첫 번째 인자는 필수 사항이며 두 번째 인자는 선택 사항이다.
또한 reduce()는 배열의 요소를 값 하나로 만드는 축소 함수이므로 배열에 프로토타입 체인을 통해 접근하여 단일값을 반환한다.
해당 메서드는 반환값이 있으므로 체이닝할 수 있다.
먼저 필수 사항인 첫 번째 인자, 콜백 함수만 넣어 배열의 합산 동작을 하는 코드를 간단하게 작성해보자.
콜백 함수의 인자는 다음과 같이 두 개만 작성해본다.
acc : accumulator. 이전 요소를 상대로 콜백 함수를 실행한 결과
cur : currentValue. 현재 요소의 값
const array = [1,2,3,4];
const result = array.reduce((acc, cur) => acc + cur);
console.log(result) // 출력: 10
이 코드는 reduce의 선택 사항인 두 번째 인자 초깃값이 설정되지 않았으므로, 다음과 같이 동작했을 것이다.
acc += cur; // 1 += 2;
acc += cur; // 3 += 3;
acc += cur; // 6 += 4;
console.log(acc); // 출력: 10
이렇게 두 번째 인자(함수에 전달할 초깃값)을 생략하면, 배열의 index[0] 의 값을 초기값으로 index[1]부터 순회하는 것을 확인할 수 있다.
그렇다면 이번엔 같은 코드에 초깃값을 설정해보자.
const array = [1,2,3,4];
const result = array.reduce((acc, cur) => acc + cur, 0);
console.log(result) // 출력: 10
이 코드 또한 같은 값 10을 반환하겠지만 실행됐을 동작을 풀어보면 다음과 같은 차이점이 있다.
acc += cur; // 0 += 1;
acc += cur; // 1 += 2;
acc += cur; // 3 += 3;
acc += cur; // 6 += 4;
console.log(acc); // 출력: 10
이렇듯 초깃값이 설정되었기 때문에, 함수는 초깃값 0을 시작으로 index[0]부터 순회하고 있다.
const array = [1,2,3,4];
const result = array.reduce((acc, cur) => acc + cur, 5);
console.log(result) // 출력: 15
그렇기 때문에 이렇게 초깃값을 5로 설정한다면, 결과는 15가 될 것이다.
const emptyArray = [];
const result = emptyArray.reduce((acc, cur) => acc + cur); // TypeError 발생
이렇게 빈 배열값에 reduce()를 초깃값 없이 사용하면 타입에러가 발생한다.
const emptyArray = [];
const result = emptyArray.reduce((acc, cur) => acc + cur, 10);
console.log(result); // 출력: 10
반면 초깃값을 설정해준 이 코드는 빈배열에 reduce를 사용했지만 타입에러가 발생하지 않고 초깃값인 10을 출력한다.
reduce()에 하나의 값만 넘기는 다른 예로, 요소가 하나인 배열에 초깃값 없이 reduce()를 사용하면 어떻게 될까?
const array = [1];
const result = array.reduce((acc, cur) => acc + cur);
console.log(result); // 출력: 1
배열의 요소 1을 정상적으로 반환하는 것을 확인할 수 있다.
reduce()는 이렇게 값이 하나만 있는 배열에서 초깃값을 생략하고 호출하거나, 빈 배열에 초깃값을 넘기면서 호출하는 등 reduce()에 값을 하나만 넘기면 그 값을 그대로 반환하며 함수는 호출하지도 않는다.
array.reduce(callback, initialValue);
이 reduce()의 기본 문법에서 필수 사항인 첫 번째 인자, 콜백 함수는 4가지의 인자를 가질 수 있다.
array.reduce((acc, cur, idx, arr) => {/*축소 함수 로직 정의*/}, initialValue);
각각
acc: accumulator. 이전 요소를 상대로 콜백 함수를 실행한 결과 (누적자)
cur: currentValue: 현재 요소의 값
idx: currentIndex: 현재 요소의 인덱스
arr: array: reduce() 메서드를 호출하는 배열
이며, 보통 앞 두 가지의 인자만 쓰는 경우가 많다. 앞서 작성한 간단한 배열 합산 함수도 acc, cur 두 인자만 사용하였다.
그렇다면 idx, arr 인자는 각각 어떤 상황에서 활용할 수 있을까?
요소의 위치가 중요할 때, 예를 들어 홀수 인덱스의 요소만 처리하는 경우에 idx 인자를 추가할 수 있다.
const numbers = [10, 20, 30, 40, 50];
const sum = numbers.reduce((acc, cur, idx) => {
if (idx % 2 !== 0) acc += cur; // 홀수 인덱스만 더함
return acc;
}, 0);
console.log(sum); // 60 (20 + 40)
원본 배열을 참조하거나 배열 길이를 활용해야 할 때 arr 인자를 추가할 수 있다.
const numbers = [10, 20, 30, 40, 50];
const avg = numbers.reduce((acc, cur, idx, arr) => {
acc += cur;
if (idx === arr.length - 1) { // 마지막 요소 처리 시
return acc / arr.length; // 평균 반환
}
return acc;
}, 0);
console.log(avg); // 30
reduceRight()는 reduce()와 비슷하지만 오른쪽에서 왼쪽으로 진행된다는 점이 다르다. 축소 동작의 결합성이 오른쪽에서 왼쪽일 경우 이 메서드가 더 유용할 수 있다. 예를 들어, 문자열을 뒤집을 때, 중첩 배열을 펼칠 때, 역순 계산이 필요할 때 등에서 사용할 수 있을 것이다.
이 중에서 역순 계산이 필요할 때인 브라우저의 뒤로가기(Undo) 기능의 예시 코드를 보자.
const history = ['Page1', 'Page2', 'Page3', 'Page4'];
const navigateBack = history.reduceRight((acc, cur, idx) => {
if (idx === history.length - 2) {
console.log(`Navigating back to: ${cur}`);
}
return acc;
});
이렇게 사용자의 히스토리는 순서대로 쌓이지만 사용자의 뒤로가기 기능을 위해서는 배열의 요소를 역순으로 조회해야할 것이다.
내가 reduce()에 대한 글을 작성하게된 것은 우테코 프리코스 중 첫주차에 작성한 forEach() 합산 로직에 대해 reduce() 리팩토링 피드백을 받았기 때문이다. 아래는 내가 기존에 작성했던 코드이다.
const sumInput = (parseNum) => {
let sum = 0;
// forEach() 메서드로 우선 입력된 값중에 너무 큰 값이 있는지 검사하고 있다면 Error 반환
// 통과하면 배열의 모든 수를 합산
parseNum.forEach(value => {
if (value > Number.MAX_SAFE_INTEGER) {
throw new Error('[ERROR] 입력된 값들 중 너무 큰 값이 있습니다.');
}
sum += value;
});
// 마지막으로 합산된 값이 infinity를 출력할 정도로 너무 크지 않은지 검사
if (sum > Number.MAX_SAFE_INTEGER) {
throw new Error('[ERROR] 합산된 값이 너무 큽니다.')
}
return sum;
}
export default sumInput;
이렇게 작성했던 코드는 크게 두 가지 측면에서 효율성이 떨어졌다.
1. 두 번 순회하는 구조: forEach()로 각 값을 순회하며 검증하고 합산 결과를 따로 계산한다는 점
2. sum을 외부 변수로 관리: sum이 외부에 선언되어 있는데, reduce()를 사용하면 누적값(accumulator)로 대체할 수 있다는 점
하지만 이 코드를 reduce()로 리팩토링을 한다면 이 두 가지 문제점을 해결할 수 있었다.
const sumInput = (parseNum) => {
// reduce()를 사용해 합산과 검증을 동시에 수행
const sum = parseNum.reduce((acc, cur) => {
if (cur > Number.MAX_SAFE_INTEGER) {
throw new Error('[ERROR] 입력된 값들 중 너무 큰 값이 있습니다.');
}
const newSum = acc + cur;
if (newSum > Number.MAX_SAFE_INTEGER) {
throw new Error('[ERROR] 합산된 값이 너무 큽니다.');
}
return newSum;
}, 0);
return sum;
};
export default sumInput;
reduce()를 사용해 중첩 배열을 펼쳐 단일 배열로 반환하기
const nestedArray = [[1, 2], [3, 4], [5, 6]];
// 결과: [1, 2, 3, 4, 5, 6]
flat() 신문법을 이용하면 배열 평탄화는 쉽게 가능하지만 reduce()를 활용해 풀어야하므로 우선 forEach()를 활용해 풀어보았다.
const nestedArray = [[1, 2], [3, 4], [5, 6]];
const result = nestedArray.reduce((acc, cur) => {
cur.forEach(i => acc.push(i));
return acc;
},[]);
console.log(result); // 결과: [1, 2, 3, 4, 5, 6]
gpt에게 다른 답안을 물어봤더니 나온 코드이다. concat() 메서드를 사용하면 이렇게 간결하게 작성할 수 있다고 한다.
const result = nestedArray.reduce((acc, cur) => acc.concat(cur), []);
console.log(result); // 결과: [1, 2, 3, 4, 5, 6]
concat() 메서드는 처음보는데, 이 메서드는 기존 배열의 요소를 포함하고 그 뒤에 concat()의 인자를 포함하는 새 배열을 만들어 반환한다고 한다. 인자에 배열이 들어있으면 배열이 아니라 그 요소를 추가한다.
concat()은 기존 배열을 수정하지 않고 원래 배열의 사본을 만들어 반환하므로 비용이 드는 작업이다. 때문에 학습하고있는 교재에서 해당 메서드에 대한 설명을 찾아보니 a = a.concat(x); 같은 코드를 자주 사용하고 있다면 push()나 splice()를 대신 쓸 수 없는지 생각해보라고한다.
const items = ['apple', 'banana', 'orange'];
const prices = [2, 1, 3]; // 개당 가격
const quantities = [3, 5, 2]; // 구매한 개수
// 결과: { total: 19, breakdown: { apple: 6, banana: 5, orange: 6 } }
const items = ['apple', 'banana', 'orange'];
const prices = [2, 1, 3];
const quantities = [3, 5, 2];
const result = items.reduce((acc, cur, idx) => {
const itemTotal = prices[idx] * quantities[idx]; // 현재 항목의 총 가격 계산
acc.total += itemTotal; // 총 구매 금액에 추가
acc.breakdown[cur] = itemTotal; // breakdown에 항목별 총 가격 추가
return acc;
}, { total: 0, breakdown: {} }); // 초기값 설정: total은 0, breakdown은 빈 객체
console.log(result); // 결과: { total: 19, breakdown: { apple: 6, banana: 5, orange: 6 } }