본문 바로가기

[JS] 비동기 흐름 : 배열 메서드 구현해보기 (Underbar) - 하

[JS] 비동기 흐름 : 배열 메서드 구현해보기 (Underbar) - 하

이해 강박증이 있기 때문에 코드를 한 줄 한 줄 우걱우걱 씹어보는 포스팅!!!!!!!

 

비동기 흐름은 JavaScript의 가장 큰 장점 중 하나이다. 비동기 흐름은 callback, promise, async/await 중 하나의 문법을 이용하여 구현할 수 있다.  

 

예전에는 배열 메서드가 브라우저에서 자체적으로 지원되지 않던 시절이 있었다. 이때 개발자들은 보다 나은 방법으로 배열이나 객체를 다루기 위한 도구 모음집을 만들었는데, 이것을 후에 라이브러리(Library)라고 부르기 시작했다.

 

배열, 객체를 다루는 Underbar라는 라이브러리를 직접 구현하면서 자바스크립트 내장 메서드가 어떻게 콜백(Callback) 함수를 활용하는지에 대한 원리를 알아보자.

 

Underbar의 모티브가 되는 라이브러리는, underscore.js, lodash 등이 있다. (여전히 JavaScript 생태계에서 쓰이고 있다.)

 

Before You Learn

  • 콜백 함수를 전달하여 사용할 수 있다.
  • 클로저 함수를 리턴하여 활용할 수 있다.
  • ... (spread syntax)를 사용하여 전달인자(arguments)의 개수를 파악할 수 있고, 각각의 전달인자에 접근할 수 있어야 한다.
  • 자바스크립트 배열 내장 메서드(forEach, map, filter, reduce 등)의 원리를 이해한다.

학습 목표

  • JavaScript의 배열 메서드를 직접 구현하며 원리를 이해한다.(slice, forEach, indexOf, filter, map, reduce 등)
  • 고차 함수(Higher order function)를 활용하여 기존에 만든 함수를 콜백 함수로 재사용할 수 있다.
  • iteratee가 콜백 함수임을 이해할 수 있다.

주의 사항

  • 극히 일부의 배열 메서드만 사용할 수 있다. 자신이 만든 함수를 활용하여, 함수 위주로 코드를 작성해야 한다.
    • 배열(Array), 집합(Set), 맵(Map)의 기본 메소드 사용은 금지된다.
    • 사용 가능한 내장 메소드: Array.prototype 의 'pop', 'push', 'shift', 'sort'
       * 단, 새로운 함수를 구현할 때 이전에 구현한 함수를 활용해도 된다.
  • console.log를 활용한 후에는 제거한다.console.log는 디버깅을 목적으로 코드 내부의 작동 방식을 파악하는 데에 도움이 된다. 그러나 console.log()를 남겨두고 배포하게 되면 문제가 생길 수 있다. 해당 메서드로 외부인이 코드 내부의 동작 방식을 쉽게 파악할 수 있기 때문이다. 이는 해킹의 우려가 있을 수 있으므로 코드 내부에서 제거해야 한다.
  • _. 에 _는 window._ = {} 를 뜻한다. 즉, window 안의 객체안에 메소드(언더바 객체)를 추가해서 쓸 수 있게 된다.

specRunner을 튼 브라우저에서 쓸 수 있게 된다.

 

개요
'use strict';
/*
  자바스크립트를 보다 효율적으로 사용하기 위해서 만들어진 underscore.js 라이브러리를
  비슷하게 구현하면서 자바스크립트의 문법을 더욱 깊게 이해하는 시간을 가지게 된다.
 */

// _.identity는 전달인자(argument)가 무엇이든, 그대로 리턴한다.
// 이 함수는 'underbar'의 기능 구현 및 테스트를 위해 재사용되는 함수이다.

_.identity = function (val) {                  
  return val;
};

 

  COLLECTIONS


  collection은 영어로 '모음, 무리'라는 뜻이다.  컴퓨터 과학에서는 '데이터(data, 자료)의 모음'으로 부를 수 있겠다.
  collection에는 다양한 종류가 있지만 배열과 객체가 대표적이다.
 
  배열은 데이터들(요소, element)을 '순서대로' 모은 자료 구조이다.(요소의 위치를 통해 데이터에 접근) 
  객체는 서로 관련있는 데이터들(속성, property)을 'key-value' 형태로 '순서 없이' 모은 자료 구조이다. 
 (속성에 부여된 키(key)를 통해 데이터에 접근)

  collection의 각 데이터를 다루는 것은 매우 흔한 작업 중에 하나이다.
    
   배열의 요소 중 가장 큰 값을 찾는 것 => 현재 데이터가 가장 큰 값인지 확인
   배열의 모든 요소의 합을 구하는 것 => 현재 데이터를 누적값에 더하기
   객체에 특정 속성의 이름(key)이 존재하는지 확인하는 것 => 현재 key가 원하는 값인지 확인하기
 
  이렇듯 각 작업들은 collection의 각 데이터를 가지고 비슷한 처리를 한다.
  이처럼 비슷한 처리가 반복되기 때문에 이를 반복(iteration) 작업이라고 부를 수 있다.


  자바스크립트는 이러한 반복(iteration) 작업을 위한 여러 수단들을 제공한다.
  반복문(for, for of, for in, while)과 반복을 위한 내장 메소드(arr.map, arr.filter)들이 대표적이다.
  사실 반복문의 제어 변수로 주로 사용하는 변수 i는 iteration의 앞 글자 i를 의미한다.


  아래의 반복문은 총 console.log가 총 세 번 반복된다.
  이때, console.log와 같이 반복되는 작업을 iteratee(반복되는 것)라고 부를 수 있다.
   

더보기

for (let i = 0; i < 3; i++) {
     console.log(i);
   }

이 반복문은 아래와 같이 해석된다.

   let i = 0;
   { // 0번째 반복
     console.log(0); // 변수 i가 0으로 치환됨
     i++;   // i는 1이 됨
   }
 
   1 < 3 이므로 다음 반복 작업을 실행
   { // 1번째 반복
     console.log(1); // 변수 i가 1로 치환됨
     i++;   // i는 2이 됨
   }
 
   2 < 3 이므로 다음 반복 작업을 실행
   { // 2번째 반복
     console.log(2); // 변수 i가 2로 치환됨
     i++;   // i는 3이 됨
   }
 
   3 < 3 이 아니므로 반복 작업 종료

 

이렇듯 collection을 다루는 여러 함수를 직접 구현해 보자.

 

 

slice
_.slice는 배열의 start 인덱스부터 end 인덱스 이전까지의 요소를 shallow copy하여 새로운 배열을 리턴한다.

// 
_.slice = function (arr, start, end) {
  // 변수를 선언할 경우, 아래와 같이 콤마(,)를 이용해 선언할 수 있다.
  // 이때, 콤마로 연결된 변수들은 모두 동일한 선언 키워드(let, const)가 적용된다.
  // 변수 앞 _ 는 암묵적 의미가 있다 (매개변수를 코드블럭 안에서 새롭게 변수를 만들어서 할당할 때) 
  
  // `start`가 *undefined(인자가 없을 때)*인 경우, slice는 0부터 동작 (||는 첫 번째 truthy 값 찾는다) **
  let _start = start || 0, 
    _end = end;             

  // 입력받은 인덱스가 음수일 경우, 마지막 인덱스부터 매칭한다. 
  // (예. -1 => arr.length - 1(마지막 arr 배열 인덱스), -2 => arr.length - 2 **
  //  arr.slice(2, -1) 은 2번째 인덱스부터 마지막 인덱스 *앞* 인덱스까지 자르는 것!)
  // 입력받은 인덱스는 0 이상이어야 한다. ****

  // 배열 길이가 5, start가 -3이라면, _start = Math.max(0, 5-3) -> 둘중 더 큰 값 : 2 
  if (start < 0) _start = Math.max(0, arr.length + start);  
  
  // 배열 길이가 5, end가 -1이라면, _end = Math.max(0, 5-1) -> 둘중 더 큰 값 : 4 
  // 만약 arr.length가 1인데 end 값이 -4다... 0을 내보냄 **
  if (end < 0) _end = Math.max(0, arr.length + end);        

  // `end`가 생략될 경우(undefined), slice는 마지막 인덱스까지 동작한다.
  // `end`가 배열의 범위를 벗어날 경우, slice는 마지막 인덱스까지 동작한다.
  if (_end === undefined || _end > arr.length) _end = arr.length;

  let result = [];
  // `start`가 배열의 범위를 벗어날 경우, 빈 배열을 리턴한다.
  for (let i = _start; i < _end; i++) {
    result.push(arr[i]);
  }
  return result;
};​

 

 

take  
_.take는 배열의 처음 n개의 element를 담은 새로운 배열을 리턴한다.

첫 번째 방법

// n이 undefined(값 생략)이거나 음수인 경우, 빈 배열을 리턴한다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
_.take = function (arr, n) {
// (배열길이가 4고, n이 3이라면 처음 3개, 즉 인덱스가 0~2인 요소)
  let _n = n;

  if (n > arr.length) _n = arr.length; // 끝에 제한 걸어주기 (설명1)
  // if (n < 0 || n === undefined) {return []; }  // 생략해도 된다. (설명2)
  
  let result = [];
  for (let i=0; i < _n ; i++) {
    result.push(arr[i])
  }
  return result;
};

(설명 1)

n이 배열의 길이를 벗어나는 경우, n의 값을 배열의 길이로 재할당시키지 않는다면 벗어난 자리의 값으로 undefined가 들어간다.

(설명 2)

n이 음수이거나 undefined 값이면 어차피 for문에서 let i=0으로 시작했을 때,  i<0, i<undefined 이므로 그대로 result = [] 값이 리턴된다. (아래는 i<undefined를 넣었을 때 결과)

 

두 번째 방법 (_.slice 활용)

(1)

_.take = function (arr, n) {
  let _n = n;
  if (_n === undefined || _n < 0) _n = 0;
  return _.slice(arr, 0, _n);
};

(2)

_.take = function (arr, n = 0) {
  return _.slice(arr, 0, n);   // 이렇게만 해도 된다...
};

 

(1) _.slice를 활용하는 방법...! 이때 당시에는 기존 함수를 활용할 생각을 미처 못했어서 머리가 띵했다. (재밌어!ㅜㅜ)

(2) n이 undefined(값 입력이 안 되었을 때) default 값을 0으로 주는 방법...!

+ 그리고 n<0일 때 : 이미 _.slice에서 예외처리 (0 or arr.length+n 중 더 큰 값 나오도록..) 이미 해주었기 때문에 굳이 안 해도 된다!!!!  예) _.take([1,2], -2) -> _.slice([1,2], 0, -2) -> _.slice([1,2], 0, 0)

 

drop
_.drop는 _.take와는 반대로, 처음 n개의 element를 제외한 새로운 배열을 리턴한다.

첫 번째 방법

// n이 undefined(값 생략)이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
// n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴한다.

_.drop = function (arr, n) {
// 배열길이 4고 만약 n이 2이면 처음 2개(인덱스 0,1) 제외 나머지 (인덱스 2,3) 리턴 
// 배열길이 4고 n이 3이면 처음 3개(0,1,2) 제외 나머지(마지막 인덱스 3) 리턴
// 즉, arr[n]~arr[arr.length-1]까지 리턴

  if (n < 0 || n === undefined) n = 0;                             
  // if (n > arr.length) return [];                       // 생략해도 된다.(설명1)
  
  let result = [];
  for(let i = n ; i < arr.length ; i++) {
    result.push(arr[i])
  }
  return result;
};

(설명 1)

어차피 i의 시작 값이 조건식의 값보다 크면 반복문은 돌아가지 않는다.

 

두 번째 방법 (_.slice 활용)

(1)

_.drop = function (arr, n) {
  if (n === undefined || n < 0) n = 0;
  return _.slice(arr, n, arr.length);
};

(2)

_.drop = function (arr, n = 0) {
  if (n < 0) return _.slice(arr);       // 위의 방식 _.slice(arr,0) 도 됨.
  // if (n > arr.length) return [];     // _.slice에서 이미 걸어준 조건임
  return _.slice(arr, n);               // 첫 번째 index 값만 넣으면 마지막 index 값까지 복사
};

 

 

 

last
_.last는 배열의 마지막 n개의 element를 담은 새로운 배열을 리턴한다.

첫 번째 방법

// n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴한다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다. (설명1)

_.last = function (arr, n) {
// 배열길이가 6이고 n이 4라면 마지막 4개(인덱스는 5,4,3,2) 담아야 한다. 
// 배열길이가 4고 n이 2라면 마지막 2개(인덱스 3,2) 담아야 한다. 
// -> 즉, 배열길이-n ~ 마지막까지(배열길이-1) 

// ** _.drop는 처음 n개의 element를 제외한 새로운 배열을 리턴 
// ㄴ 만약 n이 2고 배열 길이가 4이라면 처음 2개(인덱스 0,1) 제외 나머지 (인덱스 2,3) 리턴

// _.last는 마지막 n개 요소들 리턴 인덱스로 치면 (arr.length-n ~ arr.length-1) 
// _.drop 쓰면 되지 않을까? drop은 처음 n개 제외(n ~ arr.length-1), 
// 즉, _.last가 되려면 drop(arr,n)에서의 n이 arr.length-n이 되어야 한다.


  let _n = arr.length-n;
  if(n===undefined || n < 0) _n = arr.length-1;
  return _.drop(arr, _n);     // _.drop은 인덱스 _n부터 끝까지의 요소를 리턴
};



// (설명1)
// 배열길이가 3이고 n이 5라면 _n = 3-5 = -2 가 된다.
// _.drop(arr, -2) 에서 _.drop의 예외처리에 n 자리에 음수가 들어오면 
// 전체 배열을 shallow copy한 새로운 배열을 리턴하기로 되어 있으므로 
// 굳이 _.last에서 음수에 대한 예외처리를 따로 안 해도 된다.**

두 번째 방법

_.last = function (arr, n) {
  let _n = n;
  if (_n === undefined || _n < 0) _n = 1;
  return _.drop(arr, arr.length - _n);
};

// if (n === 0) return [];  생략 가능한 이유
// n이 0이고, 배열의 길이가 3이면 _.drop([1,2,3], 3) 이 되므로
// 처음 3개를 제외한 새로운 배열인 [] 가 이미 나오게 되어있다.

-> 비슷하게 풀었꾼!!!! 음수 예외처리를 안 해줘도 잘 작동하길래 원리가 궁금했는데..!!!!!

예외처리 체이닝이 되어 있었다. (방금 단어 만듬, 공식 단어가 있을 것 같다. 찾으면 수정 예정)

 


 

each
Array.prototype.forEach()

  _.each는 collection의 각 데이터에 반복적인 작업을 수행한다.

 

 


   1. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아

      (여기서 iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
   2. collection의 데이터(element 또는 property)를 순회하면서
   3. iteratee에 각 데이터를 인자로 전달하여 실행한다.

 * 예) 테스트 케이스의 collection은 letters이고,
 * iteratee는 익명함수 function(letter) { iterations.push(letter); }); 이다.
 *
 *  const letters = ['a', 'b', 'c'];
 *  const iterations = [];
 *  _.each(letters, function(letter) {
 *   iterations.push(letter);                                                           
 *  });
 *  expect(iterations).to.eql(['a', 'b', 'c']);

   iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 한다.

  • 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
  • 객체 obj를 입력받을 경우, iteratee(val, key, obj)

   이처럼 collection의 모든 정보가 iteratee의 인자로 잘 전달되어야 모든 경우를 다룰 수 있다.
   실제로 전달되는 callback 함수는 collection의 모든 정보가 필요하지 않을 수도 있다. (필요에 따라 쓰임)

첫 번째 방법

// _.each는 명시적으로 어떤 값을 리턴하지 않는다.

_.each = function (collection, iteratee) {

  if (Array.isArray(collection)) {
  // collection.forEach(iteratee) 은 배열의 메서드이므로 아래와 같이 구현해야 한다.
  let ele;
  for (let i=0; i<collection.length; i++) {
    // 인덱스, 요소, 배열
    ele = collection[i]                                                  // (고칠점1)
    iteratee(ele, i, collection)
  }
  }
  if (typeof collection === 'object' && !(Array.isArray(collection))) { // (고칠점2)
  // for (const [key, value] of Object.entries(collection)) {}
  let key, val;
  for (let i=0; i<Object.keys(collection).length; i++) {                // (고칠점3)
    // 키, 값, 객체
    key = Object.keys(collection)[i]
    val = collection[key]
    iteratee(val, key, collection)  // iteratee 가 이 값들을 하나 하나 다 돌 예정이다.
}
}
};

두 번째 방법

_.each = function (collection, iteratee) {
  if (Array.isArray(collection)) {
    for (let i = 0; i < collection.length; i++) {
      iteratee(collection[i], i, collection);
    }
  } else {
    for (let key in collection) {
      iteratee(collection[key], key, collection);
    }
  }
};

 

1. (고칠 점 1) : 가독성이 떨어지는 게 아니라면 변수 선언은 최소화하자.

2. (고칠 점 2) : collection의 값은 어차피 객체 아니면 배열이다. else를 쓰면 간단!

3. (고칠 점 3) for in(두 번째 방법) 과 for of를 이용해 수정할 수 있다.

   for (const [key, value] of Object.entries(collection)) {
   iteratee(value, key, collection)
   }

 

 

indexOf
_.indexOf는 target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴하고, 그렇지 않은 경우, -1을 리턴한다.
// _.indexOf는 target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴한다.
// 그렇지 않은 경우, -1을 리턴한다.
// target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴한다.

_.indexOf = function (arr, target) {
  // 배열의 *모든 요소에 접근*하려면, 순회 알고리즘(iteration algorithm)을 구현해야 한다.
  // 반복문을 사용하는 것이 가장 일반적이지만, 지금부터는 이미 구현한 _.each 함수를 활용한다.
  let result = -1;

  _.each(arr, function (item, index) {
    if (item === target && result === -1) {
      // && result === -1 는 target이 여러 개 있을 때 가장 낮은 index를 리턴하기 위함 ****
      result = index;
    }
  });

  return result;
};

 

 

 

filter
_.filter는 test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴한다.
// test(element)의 결과(return 값)가 truthy일 경우, 통과한다.
// test 함수는 각 요소에 반복 적용된다.

_.filter = function (arr, test) {
  const result = [];
  _.each(arr, function(ele) {
  if(test(ele)) result.push(ele);
  })
  return result;
};


// 화살표 함수를 쓴다면,

_.filter = function (arr, test) {
  const result = [];
  _.each(arr, (ele) => {
  if(test(ele)) result.push(ele);
  })
  return result;
};

 

 

reject
_.reject는 _.filter와 정반대로 test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴한다.

첫 번째 방법 (_.each 사용)

_.reject = function (arr, test) {
  let result = [];
  _.each(arr, function(ele) {
  if(!test(ele)) result.push(ele);            // _.filter 함수에 !만 추가함.
  })
  return result;
};

두 번째 방법 (_.filter 사용)

_.reject = function (arr, test) {
  let result = [];
  _.filter(arr, function (ele) {
    if (test(ele) === false) {
      result.push(ele);
    }
  });
  return result;
};

세 번째 방법 (_.filter + 화살표 함수 사용)

_.reject = function (arr, test) {
  return _.filter(arr, (val) => !test(val));
};

세 번째 방법.. 굉장히.. 간단하다...! 

 

uniq
// _.uniq는 주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴한다.
// 중복 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 한다.
// 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정한다.

_.uniq = function (arr) {
  const result = [];
  _.each(arr, function(ele) { // 일단 반복 돌리자~~~~~
  
    // 배열에 추가된 기존의 요소와 중복되는 지 확인 (반복?) -> 없으면 넣기
      if(_.indexOf(result, ele) === -1){                       // ****
        result.push(ele);
      }
      });
  return result;
};


(1) 실패한 방법 (array.prototype.include() 즉, 배열 메서드이므로 실패 케이스)
   if(!result.includes(ele)) {result.push(ele)}        

(2) 실패한 방법 ( ele in array 는 엄격한 동치 연산이 아니었다.) - 아래와 같이 indexOf는 그 자체가 === 이다.
  if(!(ele in result)) { result.push(ele); } 

 

 

map
 _.map은 iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴한다.
<for문을 이용해 map 함수를 기존에 구현해봤던 코드 비교>

function square(num) {return Math.pow(num, 2);}
function mul10(num) {return num * 10;}

예시)
let output = mapCallback(square, [2, 4, 7]);
console.log(output); // --> [4, 16, 49]
output = mapCallback(mul10, [2, 4, 7]);
console.log(output); // --> [20, 40, 70]
output = mapCallback(square, []);
console.log(output); // --> []

map 함수 구현)
function mapCallback(func, arr) {
let newArray = [];
for (i=0 ; i < arr.length ; i++) {
newArray.push(func(arr[i]));}
return newArray;
}

 

// 함수의 이름에서 드러나듯이 _.map은 배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)한다.

_.map = function (arr, iteratee) {

  // _.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴한다.
  const result = [];
  _.each(arr, function(ele, idx, collection) {
    result.push(iteratee(ele, idx, collection));                                           
  }
    );
  return result;

};

 

(사진1) map은 해당 배열의 ele(요소), idx(인덱스), collection(배열) 다 활용 가능              //               (사진2) DOM 으로도 활용 가능 

 

 

pluck
_.pluck은
1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
2. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
3. 최종적으로 새로운 배열을 리턴한다.

첫 번째 방법 (_.each 사용)

// 예를 들어, 각 개인의 정보를 담은 객체를 요소로 갖는 배열을 통해서, people = [{사람1 정보}, {사람2}, ...]
// 모든 개인의 나이만으로 구성된 별도의 배열을 만들 수 있다. age = [사람1 나이, 사람2 나이...]

// 최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 한다.
// 따라서 찾고자 하는 key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 이다.

// 정리 예시) people = [{name,'Serena', age:11}, {name: 'Harry', age:9}, ...]
// ages = _.pluck(people, 'age'); 'age' 만 추출해서 
// ages = [11, 9, ...] 이렇게 되어야 함.

_.pluck = function (arr, keyOrIdx) {     
  let result = [];
  _.each(arr, function (ele) {      // 첫 번째 ele = 배열의 첫번째 {사람1 정보}        
    result.push(ele[keyOrIdx]);      
  });
  return result;
};

두 번째 방법 (_.map 사용)

_.pluck = function (arr, keyOrIdx) {     

  let result = [];
  _.map(arr, function(ele){             
    result.push(ele[keyOrIdx]);         
  })
  return result;

세 번째 방법 (화살표 함수 사용 + 리팩토링)

_.pluck = function (arr, keyOrIdx) {     

return _.map(arr, (ele) => ele[keyOrIdx]);         

};

코드를 간결하게 짜기 위한 여러가지 고민을 해봐야겠다. (제일 먼저 화살표 함수에 익숙해지기!)

 

 

 

reduce... 최종 보스 등장!

reduce
 _.reduce는
1. 배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
2. 그 결과값을 계속해서 누적(accumulate)한다.
3. 최종적으로 누적된 결과값을 리턴한다.

 

 

 예를 들어, 배열 [1, 2, 3, 4]를 전부 더해서 10이라는 하나의 값을 리턴한다.
 각 요소가 처리될 때마다 누적되는 값은 차례대로 1, 1+2, 1+2+3, 1+2+3+4 이다.
 _.reduce는 배열이라는 다수의 정보가 하나의 값으로 축소(응축, 환원, reduction)되기 때문에 reduce라는 이름이 붙게 된 것이다. _.reduce는 입력으로 배열과 각 요소에 반복할 작업(iteratee)을 전달받는다.

 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)  _.reduce는 반복해서 값을 누적하므로 이 누적되는 값을 관리해야 한다.  따라서 _.reduce의 iteratee는 인자가 하나 더 추가되어 중간 최종 형태는 iteratee(acc, ele, idx, arr) 이다.

    누적되는 값은 보통 tally, accumulator(acc로 표기하기도 함)로 표현하거나
    목적을 더 분명하게 하기 위해 sum(합), prod(곱), total 등으로 표현하기도 한다.
    이때, acc는 '이전 요소까지'의 반복 작업의 결과로 누적된 값이고,
    ele는 반복 작업을 수행할(아직 수행하지 않은) 현재의 요소이다. 

여기서 빠뜨리면 안 되는 게 _.reduce는 세 번째 인자로 초기 값을 전달받을 수 있다는 것이다.
이 세 번째 인자로 초기 값이 전달되는 경우, 그 값을 누적값의 기초(acc)로 하여 배열의 '첫 번째' 요소부터 반복 작업이 수행되고,
초기 값이 전달되지 않은 경우, 배열의 첫 번째 요소를 누적값의 출발로 하여 배열의 '두 번째' 요소부터 반복 작업이 수행된다.
 
즉 최종_최종 형태!!!!!!
_.reduce(arr, iteratee, initVal)
iteratee(acc, ele, idx, arr)


예)
const numbers = [1,2,3];
const sum = _.reduce(numbers, function(total, number){ return total + number;}); 
초기 값이 주어지지 않았으므로, 초기 값은 배열의 첫 요소인 1이고, 두 번째 요소부터 반복 작업이 시작된다.
1 + 2 = 3; (첫 작업의 결과가 누적되어 다음 작업으로 전달)
3 + 3 = 6; (마지막 작업이므로 최종적으로 6이 리턴)

const identity = _.reduce([3, 5], function(total, number){ return total + number * number; }, 2);
초기 값이 2로 주어졌다. 첫 번째 요소부터 반복 작업이 시작된다.
2 + 3 * 3 = 11; (첫 작업의 결과가 누적되어 다음 작업으로 전달)
11 + 5 * 5 = 36; (마지막 작업이므로 최종적으로 36이 리턴)

첫 번째 방법

_.reduce = function (arr, iteratee, initVal) {
  // 첫 요소부터 시작이 된다. iteratee 가 만약 f(누적, 현재){return 누적 + 현재} 이라면
  // iteratee는 2개의 매개변수가 필요함.

  let accumulator = initVal;				     // 누적 값 initVal 로 할당(인자가 없을 수도)

  _.each(arr, function (ele, idx, collection) {  // 반복 돌린다~~~ 3개를 다 써보겠다. 
    if (initVal === undefined && idx === 0) {    // 초기값 입력 X && index가 0 일 때 
      accumulator = ele;                         // 누적 값은 첫번째 ele 값으로 시작!
    } else {								     // 그게 아니면 누적 값은 initVal 그대로.
      accumulator = iteratee(accumulator, ele, idx, collection);  
    }
  });
  return accumulator;
};

왜 acc의 값이 안 나올까?

 

 

 

 

두 번째 방법  ( 잘못된 방법)

_.reduce = function (arr, iteratee, initVal) {
 let result;
 if(typeof initVal === 'undefined'){              // initVal 인자가 안 들어왔으면 
 	initVal = arr[0]						      // initVal은 arr[0]
    result = initVal                              // 누적값도 arr[0]에서 시작
    
    // 1번째 index 부터 반복된다!!!                   ?????index 값 달라지지 않나..
    _.each(_.slice(arr,1), (val, idx, col) => {   
    	result = iteratee(result, val, idx, col)
    })
 } else {                                         // initVal 인자가 들어왔으면
     result = initVal    					      // 누적값은 initVal에서 시작 
     _.each(arr, (val, idx, col) => {
    	result = iteratee(result, val, idx, col)
    })	
  return result
};

reference로 받은 코드인데... 아무리 생각해도 slice를 사용해서 첫 번째 인자를 잘라내면 기존 배열과 비교했을 때 index 값이 달라지기 때문에 index 까지 이용하는 reduce 함수는 잘못된 값을 리턴해주는 것 같다. 아래는 콘솔창에 쳐본 결과이다.

마지막 결과 값 ac + cu + idx 를 누적해서 더해줬을 때 값의 차이가 난다. 


 

이렇게 코드를 씹고 뜯고 맛을 보았다. 문제를 풀 때는 애매했던 복합 개념들이 명확하게 머리 속에 들어왔다. 굉장히 피곤하고 맛있었다(?)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
⬆︎