reduce는 배열의 값마다 넘긴 함수를 부르고, 함수가 돌려준 값을 다음 부름에 넘겨 끝에 값 하나로 모으는 배열 메서드입니다. 합 모으기 틀을 메서드 하나로 씁니다.
합 모으기 틀(합 모으기와 세기)은 합을 0으로 두고, 하나씩 꺼내 더한 값에 total을 다시 붙이고, 끝나고 씁니다. map·filter·find가 거르기·바꿔 담기 틀을 메서드 하나로 쓰듯, 합 모으기에는 reduce가 있습니다.
이 레슨은 넘긴 함수가 돌려준 값이 어디로 가는지 따라가며 reduce와 합 모으기 틀을 바꿔 읽습니다.
1reduce — 돌려준 값이 다음 sum으로
그림의 코드에 찍는 줄을 더했습니다.
const prices = [900, 4500, 700];
const total = prices.reduce(
(sum, p) => sum + p,
0,
);
console.log(total);
6100reduce는 인자 둘을 받습니다. 3번 줄의 넘긴 함수와 4번 줄의 시작 값 0입니다.
처음 부를 때 sum은 시작 값 0이고 p는 0번 칸의 900입니다. 함수가 돌려준 900은 다음에 부를 때의 sum이 됩니다. 마지막에 돌려준 값을 reduce가 돌려주고, 거기에 total이 붙습니다.
부를 때마다 두 매개변수가 붙는 값입니다.
| 부름 | sum | p | 돌려준 값 |
|---|---|---|---|
| 첫째 | 0 | 900 | 900 |
| 둘째 | 900 | 4500 | 5400 |
| 셋째 | 5400 | 700 | 6100 |
돌려준 값이 한 줄 아래의 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...of | reduce |
|---|---|---|
| 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);
6sum이 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
undefinedsum은 부를 때마다 새 칸에 생기는 매개변수라, 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번 줄의 시작 값입니다.
정답
20sum이 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);
225const 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번 줄을 지워 봅니다. 무엇이 찍힐지 먼저 적고, 바꿔 돌려서 맞춰 봅니다.
저장하고 돌리는 법은 내 컴퓨터에서 돌려 보기에 있습니다.