본문으로 건너뛰기

reduce로 하나로 모으기

자바스크립트 목차배열레슨 39 / 76

목차

reduce는 배열의 값마다 넘긴 함수를 부르고, 함수가 돌려준 값을 다음 부름에 넘겨 끝에 값 하나로 모으는 배열 메서드입니다. 합 모으기 틀을 메서드 하나로 씁니다.

합 모으기 틀(합 모으기와 세기)은 합을 0으로 두고, 하나씩 꺼내 더한 값에 total을 다시 붙이고, 끝나고 씁니다. map·filter·find가 거르기·바꿔 담기 틀을 메서드 하나로 쓰듯, 합 모으기에는 reduce가 있습니다. 이 레슨은 넘긴 함수가 돌려준 값이 어디로 가는지 따라가며 reduce와 합 모으기 틀을 바꿔 읽습니다.

10단계 중 1단계. 가격 셋을 reduce로 더합니다. 넘긴 함수가 돌려준 값이 어디로 가는지 따라갑니다.

1reduce — 돌려준 값이 다음 sum으로

그림의 코드에 찍는 줄을 더했습니다.

const prices = [900, 4500, 700];
const total = prices.reduce(
  (sum, p) => sum + p,
  0,
);
console.log(total);
실행 결과
6100

reduce는 인자 둘을 받습니다. 3번 줄의 넘긴 함수와 4번 줄의 시작 값 0입니다. 처음 부를 때 sum은 시작 값 0이고 p는 0번 칸의 900입니다. 함수가 돌려준 900은 다음에 부를 때의 sum이 됩니다. 마지막에 돌려준 값을 reduce가 돌려주고, 거기에 total이 붙습니다.

부를 때마다 두 매개변수가 붙는 값입니다.

부름sump돌려준 값
첫째0900900
둘째90045005400
셋째54007006100

돌려준 값이 한 줄 아래의 sum으로 내려갑니다. sum은 부를 때마다 새 칸에 생기는 매개변수지만, 앞 부름이 돌려준 값을 받아 합을 이어 갑니다. 합 모으기 틀의 모으는 값 total과 같은 역할입니다.

2합 모으기 틀과 바꿔 읽기

같은 합을 for...of로 쓴 합 모으기 틀입니다(for...of로 배열 돌기).

const prices = [900, 4500, 700];
// 1. 합을 0으로
let total = 0;
// 2. 하나씩 꺼내 더하기
for (const p of prices) {
  total = total + p;
}
// 3. 끝나고 쓰기
console.log(total);
실행 결과
6100

하위 목표마다 reduce의 어느 자리인지 짝을 지으면 이렇습니다.

하위 목표for...ofreduce
1. 합을 0으로let total = 0;시작 값 0
2. 하나씩 꺼내 더하기total = total + p;(sum, p) => sum + p
3. 끝나고 쓰기반복 뒤에 total을 씀돌려준 값에 total을 붙여 씀

틀에서는 바퀴마다 total을 다시 붙여야 해서 let으로 만듭니다. reduce에서는 넘긴 함수가 더한 값을 돌려주기만 하고, 그 값을 다음 sum으로 넘기는 일은 reduce가 합니다. total은 끝에 한 번만 붙으니 const로 됩니다.

3시작 값을 빼면

시작 값은 뺄 수도 있습니다. 1절 코드에서 시작 값을 빼고, 부를 때마다 두 매개변수를 찍게 했습니다. 마지막 줄까지 무엇이 찍힐지 정한 뒤 펼쳐 보세요.

const prices = [900, 4500, 700];
const total = prices.reduce(
  (sum, p) => {
    console.log(sum, p);
    return sum + p;
  },
);
console.log(total);
실행 결과 보기
실행 결과
900 4500
5400 700
6100

예상과 다르면 그곳이 배울 곳입니다. 시작 값이 없으면 0번 칸의 900이 처음 sum이 되고, 함수는 1번 칸부터 두 번만 불립니다. 0 900은 찍히지 않습니다. 합은 그대로 6100입니다. 수를 더할 때는 0에서 시작하든 첫 값에서 시작하든 같기 때문입니다.

시작 값을 빼서 결과가 달라지는 경우는 자주 틀리는 곳에서 봅니다.

예제

메뉴 이름의 글자 수 모으기

돌려줄 식만 바꾸면 합이 아닌 것도 모읍니다. 메뉴 이름마다 글자 수를 더합니다.

const menus = ['커피', '아이스티'];
const n = menus.reduce(
  (sum, m) => sum + m.length,
  0,
);
console.log(n);
실행 결과
6

sum이 0에서 2를 거쳐 6이 됩니다. 넘긴 함수가 돌려준 값이 다음 sum이 되는 것은 1절과 같습니다.

대표 오류 — 빈 배열에 시작 값 없이

장바구니가 비었는데 시작 값을 빼면 멈춥니다.

const cart = [];
const total = cart.reduce(
  (sum, p) => sum + p,
);
console.log(total);
실행 결과
main.js:2const total = cart.reduce(                   ^ TypeError: Reduce of empty array with no initial value    at Array.reduce (<anonymous>)    at main.js:2:20

시작 값이 없으면 첫 값이 처음 sum이 되는데, 빈 배열에는 첫 값이 없습니다. 오류 메시지도 '시작 값 없이 빈 배열에 reduce'라고 말합니다. 3번 줄 뒤에 시작 값 0,을 넣으면 함수를 한 번도 부르지 않고 0을 돌려줍니다.

자주 틀리는 곳

틀린 생각: 시작 값은 빼도 결과가 같다.

3절에서 시작 값을 빼도 합이 같았으니, 언제나 빼도 될 것 같습니다. 예제의 글자 수 모으기에서 0,을 빼 봅니다.

const menus = ['커피', '아이스티'];
const n = menus.reduce(
  (sum, m) => sum + m.length,
);
console.log(n);
실행 결과
커피4

처음 sum이 0이 아니라 첫 값 '커피'이고, 함수는 '아이스티'에서 한 번만 불립니다. '커피'에 4를 더하면 글자 잇기라 '커피4'가 됩니다.

맞는 규칙: 시작 값을 빼면 첫 값이 처음 sum이 됩니다. 모으는 값이 배열의 값과 종류가 다르거나(글자 배열 → 수) 배열이 빌 수 있으면 시작 값을 꼭 씁니다.

틀린 생각: 넘긴 함수 안에서 sum을 다시 붙이면 다음 부름으로 이어진다.

틀에서는 total = total + p;로 모았으니, 함수 안에서도 sum을 다시 붙이면 될 것 같습니다.

const prices = [900, 4500, 700];
const total = prices.reduce(
  (sum, p) => {
    sum = sum + p;
    console.log(sum);
  },
  0,
);
console.log(total);
실행 결과
900
NaN
NaN
undefined

sum은 부를 때마다 새 칸에 생기는 매개변수라, 4번 줄은 이번 칸의 sum만 바꿉니다. 함수는 return 없이 끝나 undefined를 돌려주고, 그것이 다음 sum이 됩니다. undefined + 4500은 NaN입니다. 마지막에 돌려준 undefined에 total이 붙습니다.

맞는 규칙: 다음 sum은 넘긴 함수가 돌려준 값입니다. 중괄호 몸이면 return sum + p;를 씁니다. 그 return은 넘긴 함수의 칸만 끝내고, 돌려준 값은 reduce가 받아 다음 sum으로 넘깁니다(규칙 5).

틀린 생각: 값을 하나로 모을 때는 reduce를 써야 한다.

reduce가 더 짧아 보여서, 모으는 일은 모두 reduce로 써야 할 것 같습니다.

맞는 규칙: 2절의 두 코드는 같은 일을 합니다. 합 모으기 틀이 더 잘 읽히면 for...of로 써도 됩니다. 넘긴 함수가 길어져 한눈에 안 들어오면 틀로 쓰는 편이 낫습니다.

확인 문제

1. 무엇이 찍힐까요?

const cups = [3, 5, 2];
const total = cups.reduce(
  (sum, c) => sum + c,
  10,
);
console.log(total);
힌트

처음 sum은 4번 줄의 시작 값입니다.

정답
실행 결과
20

sum이 10에서 시작해 13, 18을 거치고, 마지막에 돌려준 20에 total이 붙습니다.

흔한 오답은 10입니다. 3 + 5 + 2만 더하고 시작 값 10을 뺀 것입니다(시작 값을 안 셈).

2. 합 모으기 틀과 reduce를 서로 바꿔 씁니다.

(가) 아래 틀과 같은 값을 reduce로 만들도록 빈칸을 채우세요.

const scores = [80, 55, 90];
let total = 0;
for (const s of scores) {
  total = total + s;
}
console.log(total);
실행 결과
225
const scores = [80, 55, 90];
const total = scores.reduce(
  ________
  0,
);
console.log(total);
힌트

틀에서 total에 더하던 식을 함수가 돌려줍니다. 앞 부름까지의 합이 어느 매개변수에 오는지 1절 표에서 찾습니다.

정답

(sum, s) => sum + s,입니다.

const scores = [80, 55, 90];
const total = scores.reduce(
  (sum, s) => sum + s,
  0,
);
console.log(total);
실행 결과
225

흔한 오답은 (sum, s) => total + s,⁠입니다. 이 코드는 const total이 아직 만들어지지 않은 때 total을 읽어 멈춥니다. 앞 부름까지의 합은 sum에 있습니다.

(나) 아래 reduce와 같은 값을 만드는 합 모으기 틀을 for...of로 쓰세요. 6이 찍혀야 합니다.

const counts = [2, 1, 3];
const n = counts.reduce(
  (sum, c) => sum + c,
  0,
);
console.log(n);
힌트

2절 짝 표를 거꾸로 읽습니다. 시작 값이 '1. 합을 0으로'의 0이고, 함수가 돌려주는 식이 바퀴마다 다시 붙일 값입니다.

정답
const counts = [2, 1, 3];
let n = 0;
for (const c of counts) {
  n = n + c;
}
console.log(n);
실행 결과
6

다르게 써도 되는 곳: 꺼낸 값의 이름은 count처럼 지어도 됩니다.

흔한 오답은 const n = 0;으로 만든 것입니다. 틀에서는 바퀴마다 n을 다시 붙이니, 4번 줄에서 TypeError로 멈춥니다. reduce에서 const가 됐던 것은 끝에 한 번만 붙었기 때문입니다.

3. 무엇이 찍힐까요?

const prices = [900, 4500, 700];
const total = prices.reduce(
  (sum, p) => p,
  0,
);
console.log(total);
힌트

함수가 돌려주는 식에 sum이 들어 있는지 봅니다.

정답
실행 결과
700

함수는 sum을 쓰지 않고 이번 값 p를 그대로 돌려줍니다. 돌려준 값은 900, 4500, 700이고, 마지막에 돌려준 700에 total이 붙습니다.

흔한 오답은 6100입니다. 이름이 sum이고 reduce를 썼으니 알아서 더한다고 본 것입니다. 더하는 일은 함수의 식이 하고, reduce는 돌려받은 값을 넘길 뿐입니다(규칙 6).

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 1절 코드를 main.js에 저장해 돌린 뒤, 4번 줄의 시작 값 0을 1000으로 바꿔 봅니다. 다음에는 4번 줄을 지워 봅니다. 무엇이 찍힐지 먼저 적고, 바꿔 돌려서 맞춰 봅니다. 저장하고 돌리는 법은 내 컴퓨터에서 돌려 보기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

자바스크립트 목차로