TIL | 함수형 JS - map, filter, reduce

noopy·2021년 8월 10일
0

TIL

목록 보기
4/21
post-thumbnail

Week2. 함수형 자바스크립트와 map, filter, reduce

map, filter, reduce

🍡 map

1. return 활용하기

const products = [
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
  {name: '원피스', price: 25000}
];

const map = () => {
  const names = [];
  for (const value of products) {
    names.push(products.name);
  }
  console.log(names); // ❌ names를 변화를 일으키는 메서드나 함수에 보내지 않음.
  return names; // 🙆‍♀️리턴된 값을 이후에 사용하도록 함.
}

함수형 프로그래밍에선 인자와, 리턴 값으로 소통한다.
내부의 변수를 console.log와 같이 변화를 일으키는 함수나 메서드에 보내지 않고,
return을 통해 리턴된 값을 개발자가 이후에 활용하도록 한다.

2. 내부로직 추상화 하기

const products = [
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
  {name: '원피스', price: 25000}
];

const map = (func, iterator) => {
  const names = [];
  for (const value of iterator) {
    names.push(func(value)); // 어떤 값을 수집할 것인지 func 함수에게 위임함.
  }
  return names;
}

console.log(map(value => value.name, products)); // [반팔티, 긴팔티, 원피스]

인자를 iterator라 한 이유는 products가 일전에 말한 iterable / iterator 프로토콜을 따르는 배열이기 때문에 iterator라 표현하였다.

함수형 프로그래밍에선 명령형 프로그래밍에서처럼 하나하나 로직을 설명하지 않는다. 함수에게 해당 역할을 위임해 추상화한다. 위 예시에서 product.name을 조회한 것과 달리 해당 역할은 func 함수가 인자로 iterator의 value를 받아 return 하는 식으로 처리할 것이다.

또한 여기서 map 함수는 고차함수이다.
함수를 값으로 다루고 원하는 시점에 인자를 적용하기 때문이다.

map이 호출되어 최종 값을 출력하는 과정을 순서대로 설명해보겠다.

  1. map이 호출되면 인자로 value => value.name, products를 각각 func와 iterator에 전달한다.
    func = value => value.name, iterator = products
  2. for of문을 돌면서 iterator의 value 들을 하나씩 조회한다.
    첫번째 순회만 예시로 들어본다면,
    value = {name: '반팔티', price: 15000}이고 func의 인자로 value를 넘기며 func 함수가 호출된다. value.name = 반팔티 가 리턴되고 names 변수에 반팔티를 push 한다.
  3. map을 통해 최종적으로 [반팔티, 긴팔티, 원피스]가 콘솔에 찍힐 것이다.

iterable 프로토콜을 따르는 map의 다형성

nodeList를 map에 담아 배열로 출력할 수 있을까?

const nodeList = document.querySelectorAll('*'); // [html, head, meta, meta, title, link, script, body, script]
console.log(nodeList.map(element => element))) // nodeList.map is not a function

nodeList는 배열처럼 보이지만 배열이 아니다.
nodeList는 array를 상속받은 객체가 아니고, nodeList[[Prototype]]에 map이 없기 때문에 map을 사용하려해도 사용할 수 없다.
하지만 위에서 구현한 map 함수를 통해 nodeList를 배열로 출력할 수 있다.

const map = (func, iterator) => {
  const names = [];
  for (const value of iterator) {
    names.push(func(value)); // 어떤 값을 수집할 것인지 func 함수에게 위임함.
  }
  return names;
}

const nodeList = document.querySelectorAll('*');

console.log(map(value => value.nodeName, nodeList)) // [html, head, meta, meta, title, link, script, body, script]

map 함수는 iterable / iterator 프로토콜을 따르는 많은 함수들을 다양하게 활용할 수 있다.

let iterator = document.querySelectorAll('*')[Symbol.iterator]();
console.log(iterator.next()) // {value: html, done: false};
console.log(iterator.next()) // {value: head, done: false};
console.log(iterator.next()) // {value: meta, done: false};

nodeList는 iterable / iterator 프로토콜을 따르고 있다. 또한 map 함수는 내부적으로 iterable / iterator 프로토콜을 따르는 for of문을 사용하기 때문에 iterator 쪽에 iterable / iterator 프로토콜을 따르는 것들은 다 활용이 가능한 것이다.

(프로토콜 때문에 끝말잇기 하는 기분이 드네...iterable / iterator 프로토콜 이름이 너무 길어서 앞으로는 iterable 프로토콜이라 부르겠다. 😂)

🍡 filter

filter는 특정 조건을 만족하는 것들을 모아 배열로 return 한다.

const products = [
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
  {name: '원피스', price: 25000}
];

const filter = (func, iterator) => {
  const arr = [];
  for (const value of iterator) {
   if(func(value)) arr.push(value);
  }
  return arr;
};
    
console.log(filter(value => value.price < 20000, products));

func(value)는 조건을 판별하기 때문에 boolean 값을 리턴한다.

🍡 reduce

reduce는 iterable 값을 다른 값으로 축약한다.

const nums = [1, 2, 3, 4, 5];

const reduce = (func, acculator, iterator) => {
  for (const value of iterator) {
	acculator = func(acculator, value);
  }
  return acculator;
}

console.log(reduce((a, b) => a + b, 0, nums));

👇🏻 과정 설명

func = a, b => a + b;
acculator = 0;
1. func 함수가 인자로 0과 nums의 첫번째 요소 1을 전달받으면 0 + 1 값을 리턴한다.
2. 1 (0 + 1) 은 다음 순회 때 acculator가 되어 nums의 두번째 요소 2와 더해진다.
3. 순회가 끝나면 최종 누적 값인 15를 반환한다.

acculator를 생략하는 reduce

JS에서는 reduce에 acculator가 없어도 작동되도록 만들어져 있다.
예를 들어 [1, 2, 3, 4, 5] 라는 배열이 있다면, 첫번째 순회 때 1 [2, 3, 4, 5]의 형태 (1을 꺼내 기본 값으로 설정)로 작동된다.

그럼 acculator를 옵셔널하게 사용할 수 있도록 코드를 수정해보자.

const nums = [1, 2, 3, 4, 5];

const reduce = (func, acculator, iterator) => {
  if (!iterator) { // acculator 자리가 iterator로 당겨졌기 때문에 iterator가 없는거임.
    iterator = acculator[Symbol.iterator](); // 해당 객체에서 iterator를 반환하도록 함.
    acculator = iterator.next().value;
  }
  
  for (const value of iterator) {
	acculator = func(acculator, value);
  }
  return acculator;
}

console.log(reduce((a, b) => a + b, 0, nums));
console.log(reduce((a, b) => a + b, nums)); // acculator가 없을 경우

acculator가 생략되면 기존 iteratoracculator 자리로 당겨지고 iterator가 없어지게 된다. 따라서 (진짜 생략된 것은 acculator 이지만) iterator의 존재 여부를 체크해 없을 경우, acculator가 iterator를 반환해 iterator가 원래 역할을 할 수 있게 만들고, acculatoriterator의 next 메서드 속 value가 되도록 한다.

👇🏻 과정 설명

acculator가 없어져 iterator가 앞으로 당겨졌을 경우
1. iterator 존재 여부 체크해 iterator 변수에 iterator를 반환.
2. acculatoriterator.next().value = 1이 됨.
3. 이때 next() 메서드를 사용했으므로 iterator는 2번째 요소를 가리키는 상태임.
{value: 2, done: false}
4. 반복문을 시작하며 1과 2를 더함.
5. 순회가 끝난 후, 최종값 15 리턴

🍡 map + filter + reduce 중첩 사용

const products = [
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
  {name: '원피스', price: 25000},
  {name: '후드티', price: 30000}
];

// 가격을 뽑아 map 만들기
const map = (func, iterator) => {
  const arr = [];
  for (const value of iterator) {
    arr.push(func(value))
  }
  return arr;
}

console.log(map(value => value.price, products));
// [15000, 20000, 25000, 30000];

// 3만원 미만 상품들의 가격만 배열로 만들기
const filter = (func, iterator) => {
  const arr = [];
  for (const value of iterator) {
  if (func(value)) arr.push(value);
  }
  return arr;
}

console.log(map(value => value.price,
                filter(value => value.price < 30000, products)));
// [15000, 20000, 25000];

// 3만원 미만의 상품들의 가격을 다 합치기
const reduce = (func, acculator, iterator) => {
  for (const {name, price} of iterator) {
    acculator = func(acculator, price);
  }
  return acculator;
}

const add = (a, b) => a + b;

console.log(reduce(
  add, 
  0, 
  map(value => value.price,
      filter(value => value.price < 30000, products))));
// 60000;

느낀점

함수형 프로그래밍에서 추상화에 대해 이론적으로만 이해하고 실제 구현은 어떤 식으로 이루어지는지 궁금했는데 오늘 이해한 것 같다.
보조 함수를 만들어 보조 함수가 해당 기능을 수행하도록 위임해 내부 로직에 대해 알 필요가 없다는 점? 또 함수가 값으로 활용되기 때문에 조합하기 좋아 이리저리 다른 함수와 조합한다는 점?이 있겠다.
아직까지는 기존의 map, filter, reduce가 어떤 방식으로 작동되는지 알았기 때문에 이해하기 쉬웠다면 앞으로 go와 pipe를 활용해보며 점점 심화단계로 나아갈 것 같다 🔥

profile
💪🏻 아는 걸 설명할 줄 아는 개발자 되기

0개의 댓글