프로그래밍에서 요소들을 순회하면서 특정한 작업을 하는 경우는 매우 많습니다.
함수형 프로그래밍에서는 이러한 작업들을 좀 더 보기 쉽도록 해주는 함수들을 사용하여 요소들을 순회합니다.
map, filter, reduce 함수란?
대표적인 예시가 `map`, `filter`, `reduce` 입니다.
`map` 함수는 요소를 순회하면서 요소마다 작업을 하는 경우에 사용됩니다.
`filter` 함수는 요소를 순회하면서 특정 조건을 만족하는 요소들만 필터링할 때 사용됩니다.
`reduce` 함수는 요소를 순회하면서 내부적으로 특정 함수를 재귀적으로 호출하려는 경우에 사용됩니다.
이제 각각의 함수들을 만들어보도록 합시다.
1) map 함수
먼저 다음과 같이 순회할 수 있는 배열이 있습니다.
type Product = {
name: string;
price: number;
};
const products: Product[] = [
{ name: "반팔티", price: 15000 },
{ name: "긴팔티", price: 20000 },
{ name: "핸드폰케이스", price: 15000 },
{ name: "후드티", price: 30000 },
{ name: "바지", price: 25000 },
];
일반적인 JS의 반복문을 사용하여 요소를 순회하면 다음과 같은 코드가 될 것입니다.
// map 이전의 코드
const names: string[] = [];
for (const p of products) {
names.push(p.name);
}
console.log(names);
const prices: number[] = [];
for (const p of products) {
prices.push(p.price);
}
console.log(prices);
그렇다면 요소를 순회하면서 특정 작업을 계속 수행할 수 있는 map 함수를 만들어봅시다.
이때, 저희가 고려해야할 것이 2가지가 있습니다.
- 함수형 프로그래밍은 함수가 인자와 반환값으로만 소통하는 것을 권장합니다.
- 외부 변수(전역 변수 등)에 의존적인 함수가 되어서는 안됩니다.
이 2가지를 모두 고려한 `map`함수를 만들어 봅시다.
const map = (f: Function, iter: Iterable<any>) => {
const res: any[] = [];
for (const item of iter) {
res.push(f(item));
}
return res;
};
우리가 만든 `map`함수를 이용하면 결과가 잘 나옴을 확인할 수 있습니다.
console.log(map((p: Product) => p.name, products));
console.log(map((p: Product) => p.price, products));
배열에 있는 `map` 메서드를 사용하지 않고, 왜 `map` 함수를 따로 만들었을까요?
왜냐하면 이터러블 프로토콜을 따른 모든 데이터에서 요소를 순회할 수 있도록 하기 위함입니다.
`document.querySelectorAll()` 함수가 반환해주는 NodeList는 Array를 상속받지 않음으로 다음처럼 `map()` 메서드를 사용하면 에러가 발생합니다.
const allElems = document.querySelectorAll("*"); // NodeList
allElems.map((el) => el.nodeName); // ❌: Error
하지만 위에서 만들었던 `map` 함수를 사용하면 에러가 발생하지 않습니다. 왜냐하면 `NodeList`는 이터러블 프로토콜을 따르기 때문이죠.
map((el: Element) => el.nodeName, allElems);
const iter = document.querySelectorAll("*")[Symbol.iterator]();
추가적으로 제네레이터 함수가 반환해주는 이터러블에도 `map` 함수를 사용할 수 있습니다.
function* gen() {
yield 2;
yield 3;
yield 4;
}
console.log(mapV3((a: number) => a * a, gen()));
2) filter 함수
`filter` 함수는 특정 조건을 만족시키는 요소들만 필터링할 때 사용하는 함수입니다.
일단 for문을 사용하여 필터링을 진행해보도록 합시다.
// filter
const under20000: Product[] = [];
for (const p of products) {
if (p.price < 20000) under20000.push(p);
}
console.log(under20000);
const over20000: Product[] = [];
for (const p of products) {
if (p.price >= 20000) over20000.push(p);
}
console.log(over20000);
`map` 함수와 마찬가지로 외부에 의존성이 없는 함수로 만들어보도록 합시다.
const filter = (f: Function, iter: Iterable<any>) => {
const res: Product[] = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};
console.log(filter((p: Product) => p.price < 20000, products));
console.log(filter((p: Product) => p.price >= 20000, products));
완성이 된 것을 볼 수 있죠.
3) reduce 함수
특정 함수를 중첩해서 실행하여 하나의 결과로 만드는 함수이며, 내부적으로 재귀적으로 실행되는 함수입니다.
먼저 for 문을 이용하여 배열의 모든 수를 더하는 함수를 만들어봅시다.
// reduce
const nums = [1, 2, 3, 4, 5];
let sum = 0;
for (const n of nums) {
sum += n;
}
console.log(sum);
매우 간단한 코드입니다. 이를 `reduce` 함수를 통해서 작성해봅시다.
const reduce = (f: Function, acc: any, iter: Iterable<any>) => {
for (const item of iter) {
acc = f(acc, item);
}
return acc;
};
const add = (a: number, b: number) => a + b;
console.log(reduce(add, 0, [1, 2, 3, 4, 5])); // 15
이렇게 코드를 작성한다면 실제 내부동작은 다음과 같이 함수 내부에서 재귀적으로 실행될 것입니다.
add(add(add(add(add(0, 1), 2), 3), 4), 5);
그런데 실제 `reduce` 함수는 `acc`라는 매개변수를 옵셔널하게 실행할 수 있도록 해줍니다.
즉 `reduce(add, [1, 2, 3, 4, 5])` 이렇게도 실행할 수 있으며, 이는 `reduce(add, 1, [2, 3, 4, 5])` 이렇게 실행됩니다.
그렇다면 `acc`를 옵셔널하게 사용할 수 있도록 리팩토링해봅시다.
const reduce = (f: Function, acc: any, iter?: any) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const item of iter) {
acc = f(acc, item);
}
return acc;
};
const add = (a: number, b: number) => a + b;
console.log(reduce(add, [1, 2, 3, 4, 5])); // 15
그럼 다음과 같이 3번째 매개변수의 존재 여부에 따라서, 함수의 내부 실행이 조금 변경이 되었습니다.
만약 3번째 매개변수가 존재하지 않는다면 2번째 매개변수에 이터러블이 들어갔다고 가정하고, 이터러블의 `Symbol.iterator` 메서드를 활용해 이터레이터를 꺼낸 후, 첫 번째 요소를 `acc`로 사용하도록 해줍니다.
그 외에는 내부적으로 함수를 중첩해서 사용하는 것은 동일합니다.
map, filter, reduce를 종합적으로 사용해보도록 합시다.
이제, 만들어두었던 모든 함수를 다 사용해서 코드를 작성해보도록 합시다.
type Product = {
name: string;
price: number;
};
const products: Product[] = [
{ name: "반팔티", price: 15000 },
{ name: "긴팔티", price: 20000 },
{ name: "핸드폰케이스", price: 15000 },
{ name: "후드티", price: 30000 },
{ name: "바지", price: 25000 },
];
const add = (a: number, b: number) => a + b;
const total = reduce(
add,
0,
map(
(p: Product) => p.price,
filter((p: Product) => p.price < 20000, products)
)
);
console.log(total);
중첩함수 형태로 실행이 되며 오른쪽에서 왼쪽으로 읽어나가면 됩니다.
이렇게 중첩함수들을 모두 사용하게 된다면, 가독성이 조금 떨어진다는 단점은 존재합니다. 이를 보완하기 위한 여러 기법들을 다음에 배워보도록 하겠습니다.