배열 메서드 — map, filter, reduce
Article
9강에서 배열을 돌 때 for문을 썼다. 이제 더 나은 방법을 배운다.
11강에서 배운 콜백이 여기서 진가를 발휘한다. "어떻게 반복할지"는 배열에 맡기고, 우리는 "각 요소로 무엇을 할지"만 말한다.
forEach — 그냥 하나씩 돈다
const fruits = ["사과", "바나나", "포도"];
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
fruits.forEach(fruit => console.log(fruit));
- 사과
- 바나나
- 포도
- 사과
- 바나나
- 포도
i도, length도, fruits[i]도 없다. 실수할 자리가 사라졌다.
콜백은 인자를 세 개까지 받는다. 필요한 만큼만 쓰면 된다.
["a", "b"].forEach((value, index, array) => {
console.log(index, value, array.length);
});
- 0 'a' 2
- 1 'b' 2
그리고 forEach는 아무것도 돌려주지 않는다. 값을 만들어내려면 다른 메서드가 필요하다.
map — 하나씩 바꿔서 새 배열을 만든다
가장 많이 쓰는 메서드다.
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
console.log(doubled);
console.log(numbers);
- [2, 4, 6]
- [1, 2, 3]
원본은 그대로다. 길이가 같은 새 배열이 만들어진다.
- [1, 2, 3]원본
- n => n * 2각 요소에 적용
- [2, 4, 6]새 배열 (길이 같음)
객체 배열에서 특정 값만 뽑을 때 자주 쓴다.
const users = [
{ name: "지원", age: 27 },
{ name: "민수", age: 31 },
];
const names = users.map(user => user.name);
console.log(names);
- ['지원', '민수']
filter — 조건에 맞는 것만 남긴다
콜백이 true를 돌려준 요소만 살아남는다.
const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter(n => n % 2 === 0);
const big = numbers.filter(n => n > 4);
console.log(evens);
console.log(big);
- [2, 4, 6]
- [5, 6]
map과 달리 길이가 줄어든다. 하나도 안 맞으면 빈 배열이 된다.
map
변환
- 길이가 그대로
- 콜백의 반환값이 새 요소
- "각각을 무엇으로 바꿀까"
filter
선별
- 길이가 줄어든다
- 콜백의 참·거짓으로 판단
- "각각을 남길까 버릴까"
7강의 truthy·falsy가 여기서 쓰인다. filter(Boolean)은 falsy 값을 전부 걷어내는 관용구다.
const messy = [1, null, 2, "", 3, undefined, 0];
console.log(messy.filter(Boolean));
- [1, 2, 3]
reduce — 전부 하나로 접는다
가장 어렵지만 가장 강력하다. 배열을 훑으며 값 하나로 줄여나간다.
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, cur) => acc + cur, 0);
console.log(sum);
- 10
콜백이 인자를 둘 받는다. acc는 지금까지 쌓인 값(accumulator), cur는 이번 요소(current)다. 마지막 0은 초기값이다.
- 01
초기값에서 시작
acc는0. 아직 아무것도 더하지 않았다.acc = 0 - 02
첫 요소
acc(0) + cur(1)→ 다음acc는1.0 + 1 = 1 - 03
계속 접는다
1 + 2 = 3,3 + 3 = 6,6 + 4 = 10. - 04
마지막 acc 가 결과
돌 게 없으면
acc를 반환한다.console.log(sum); // 10
숫자만 더하는 게 아니다. 객체를 만들 수도 있다.
const words = ["apple", "banana", "avocado", "blueberry"];
const grouped = words.reduce((acc, word) => {
const key = word[0];
if (!acc[key]) acc[key] = [];
acc[key].push(word);
return acc;
}, {});
console.log(grouped);
- { a: ['apple', 'avocado'], b: ['banana', 'blueberry'] }
찾기 — find, some, every
filter는 전부 훑지만, 하나만 찾으면 되는 경우가 있다.
| 메서드 | 돌려주는 것 | 언제 멈추나 |
|---|---|---|
| find | 첫 번째로 맞는 요소 | 찾는 즉시 |
| findIndex | 첫 번째로 맞는 인덱스 | 찾는 즉시 |
| some | 하나라도 맞으면 true | 맞는 걸 찾는 즉시 |
| every | 전부 맞아야 true | 안 맞는 걸 찾는 즉시 |
const users = [
{ name: "지원", age: 27 },
{ name: "민수", age: 15 },
];
console.log(users.find(u => u.age < 19));
console.log(users.some(u => u.age < 19));
console.log(users.every(u => u.age < 19));
console.log(users.find(u => u.age > 100));
- { name: '민수', age: 15 }
- true
- false
- undefined
find는 못 찾으면 undefined를 준다. 15강의 indexOf가 -1을 주던 것과 다르다.
some은 6강의 ||처럼 너그럽고, every는 &&처럼 까다롭다.
체이닝 — 이어붙인다
이 메서드들은 모두 배열을 돌려주므로 점으로 이어 쓸 수 있다.
const products = [
{ name: "노트북", price: 1200000, inStock: true },
{ name: "마우스", price: 30000, inStock: true },
{ name: "모니터", price: 400000, inStock: false },
];
const total = products
.filter(p => p.inStock)
.map(p => p.price)
.reduce((acc, price) => acc + price, 0);
console.log(total);
- 1230000
읽어보면 그대로 문장이다. "재고가 있는 것만 골라서, 가격만 뽑아서, 전부 더한다."
for문으로 쓰면 같은 일이 이렇게 된다.
let total = 0;
for (let i = 0; i < products.length; i++) {
if (products[i].inStock) {
total += products[i].price;
}
}
동작은 같지만 무엇을 하는지가 아니라 어떻게 도는지가 먼저 보인다.
정리하면
forEach는 실행만 한다. 값을 돌려주지 않고 멈출 수도 없다.map은 길이를 유지하며 변환하고,filter는 조건으로 걸러낸다.reduce는 배열을 값 하나로 접는다. 콜백은 다음acc를 반드시 반환하고, 초기값을 준다.- 하나만 찾으면
find, 존재 여부는some, 전부 만족하는지는every. - 이 메서드들은 원본을 바꾸지 않는다. 체이닝으로 이어 쓴다.
다음 강의는 객체다. 배열이 순서로 값을 찾는다면, 객체는 이름으로 값을 찾는다.