본문 바로가기

Frontend Study

[FE_Bootcamp] 24일차_Underbar

언제나 그랬듯 금요일에 일이 있어 나갔다가 밖에서 자고, 토요일에 김포까지 가서 오랜만에 풋살을 하고 왔다.

집에 돌아와 쓰러졌다 일어나니 저녁 9시. 

결국 오늘로 블로그 작성을 미루게 되었다. 게으른 나!

 

여튼, 24일차에 했던 것은 Underbar 과제였다. 

Underbar 과제는 고차함수 단원의 연장선으로, '각종 메서드들의 작동 원리를 이해하고 실제 함수 내부를  직접 구현'하는 것이었다. 

단, Array, Math, Object 등의 '기본 내장 메서드'들은 일부를 제외하곤 전혀 사용할 수 없다!

항상 완성된 상태로만 쓰던 메서드들을 직접 구현해 보라니,..,,.그것도 메서드 없이.,.,.,.조금 어지러워지려 했다.

하지만, 언제까지 기계처럼만 쓸 순 없는 법! 시간도 넉넉하게 주었겠다, 바로 진행해 보았다.

 

내용이 꽤 많음으로, 구현해야 할 내용(조건), 구현한 코드, 의사코드 정도만 작성하도록 하겠다.

1. _.take

// _.take는 배열의 처음 n개의 element를 담은 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 빈 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
_.take = function (arr, n) {
  let result = []
  if (n > arr.length) {
    for (let i = 0; i < arr.length; i++) {
      result.push(arr[i])
    }
  } else {
    for (let i = 0; i < n; i++) {
      result.push(arr[i])
    }
  }
  return result
};
 
 
  •  빈 배열 result 선언
  • n이 배열의 길이보다 길 경우, for문을 통해 arr의 전체 요소를 result에 push
  • n이 배열의 길이보다 짧을 경우, for문을 통해 arr의 0번 요소부터 (n-1)번 요소를 result에 push
  • 이외의 경우 result에 아무것도 push되지 않으므로 빈 배열 return

2. _.drop

// _.drop는 _.take와는 반대로, 처음 n개의 element를 제외한 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴합니다.
_.drop = function (arr, n) {
  let result = []
  if(n < 0 || n === undefined){
    for (let i = 0; i < arr.length; i++) {
      result.push(arr[i])
    }
  } else {
    for (let i = n; i < arr.length; i++) {
      result.push(arr[i])
    }
  }
  return result
};

 

  •  빈 배열 result 선언
  • n이 0보다 작거나 undefined인 경우, for문을 통해 arr의 전체 요소를 result에 push
  • 이외의 경우 for문을 통해 n번 요소부터 arr의 끝까지 result에 push
  • n이 배열의 길이를 벗어날 경우, arr.length < n이 되어 for문이 작동하지 않으므로 빈 배열 return

3. _.last

// _.last는 배열의 마지막 n개의 element를 담은 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
// _.take와 _.drop 중 일부 또는 전부를 활용할 수 있습니다.
_.last = function (arr, n) {
  let result = []
  if(n < 0 || n === undefined){
    result.push(arr[arr.length - 1])
  } else if(n > arr.length){
    for (let i = 0; i < arr.length; i++) {
      result.push(arr[i])
    }
  } else {
    for (let i = arr.length - n; i < arr.length; i++) {
      result.push(arr[i])
    }
  }
  return result
};

 

 

 

  • 빈 배열 result 선언
  • n이 0보다 작거나 undefined인 경우, arr의 마지막 요소만 result에 push
  • n이 배열의 길이를 벗어날 경우, for문을 통해 arr의 전체 요소를 result에 push
  • 이외의 경우 arr의 마지막에서 n 번째 요소부터 마지막 요소까지 result에 push

4. _.each

// _.each는 collection의 각 데이터에 반복적인 작업을 수행합니다.
//  1. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
//  2. collection의 데이터(element 또는 property)를 순회하면서
//  3. iteratee에 각 데이터를 인자로 전달하여 실행합니다.
// 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)){
    for(let i = 0; i < collection.length; i++){
      iteratee(collection[i], i, collection)
    }
  } else if(typeof collection === 'object'){
    for(let key in collection){
      iteratee(collection[key], key, collection)
    }
  }

 

 

  • collection이 배열이라면, for문을 통해 각 배열의 요소, 인덱스, 배열을 iteratee 함수의 전달인자로 전달
  • collection이 객체라면, for-in문을 통해 각 배열의 key, value, 객체를 iteratee 함수의 전달인자로 전달

5. _.indexOf

// _.indexOf는 target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴합니다.
// 그렇지 않은 경우, -1을 리턴합니다.
// target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴합니다.
_.indexOf = function (arr, target) {
  let result = -1;
  _.each(arr, function (item, index) {
    if (item === target && result === -1) {
      result = index;
    }
  });

  return result;
};

 

  • result를 -1로 설정
  • 앞서 생성한 _.each 함수를 통해 arr의 각 요소에 접근
  • _.each 함수 내부의 iteratee = function으로 선언한 함수. 매개변수가 2개이므로 'arr의 요소'가 item, 'item의 인덱스'가 index가 됨
  • item(배열의 요소)가 target과 같으면, result를 index(item의 인덱스)로 바꿈
  • indexOf는 중복 요소가 여러개인 경우 가장 앞의 인덱스만 반환하기 때문에, result가 -1인 상태에서 item = target인 경우에만 item의 index를 return함. 따라서 && 연산자를 통해 두 조건을 합침

indexOf 함수 같은 경우에는 미리 작성되어 있던 코드인데, 뒤에 사용할 일이 몇번 있어 함께 적어둔다.

 


6. _.filter

// _.filter는 test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴합니다.
// test(element)의 결과(return 값)가 truthy일 경우, 통과입니다.
// test 함수는 각 요소에 반복 적용됩니다.
_.filter = function (arr, test) {
  let result = []
  _.each(arr, function(item){
    if(test(item)){
      result.push(item)
    }
  })
  return result
};

 

 

  • 빈 배열 result 생성
  • 앞서 생성한 _.each 함수를 통해 arr의 각 요소에 접근
  • _.each 함수 내부의 iteratee = function으로 선언한 함수. 매개변수가 1개이므로 'arr의 요소'가 item이 됨
  • _.each 함수는 배열 또는 객체의 모든 요소를 순회하는 함수이므로, test(item)은 모든 요소에 test() 함수를 적용한 것과 같음
  • test(item)이 true라면, item을 result에 추가, false 일 경우 패

7. _.reject

// _.reject는 _.filter와 정반대로 test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴합니다.
_.reject = function (arr, test) {
  let result = []
  _.each(arr, function(item){
    if(!test(item)){
      result.push(item)
    }
  })
  return result
};

 

  • 빈 배열 result 생성
  • filter와 작동 원리는 같으나, 통과하지 않는 요소를 찾는 것이기 때문에 test(item)이 false인 경우에만 result에 요소 추가

8. _.uniq

// _.uniq는 주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴합니다.
// 중복 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 합니다.
// 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정합니다.
_.uniq = function (arr) {
  let result = []
  _.each(arr, function(item){
    if(_.indexOf(result, item) === -1){
      result.push(item)
    }
  })
  return result
};

 

  • 빈 배열 result 생성
  • _.each 함수를 통해 배열 요소 순회
  • arr의 요소 item이 result에 들어있다면(중복이라면), indexOf(result, item)은 해당 요소의 인덱스가 됨. 
  • 중복이 아니라면, -1을 반환하므로 'indexOf(result, item) === -1'은  'result에 item과 같은 요소가 없다'는 의미
  • 중복이 아니므로, result에 item을 push

9. _.map

// _.map은 iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴합니다.
// 함수의 이름에서 드러나듯이 _.map은 배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)합니다.
_.map = function (arr, iteratee) {
  // _.map 함수는 매우 자주 사용됩니다.
  // _.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴합니다.
  let result = []
  _.each(arr, function(item){
    result.push(iteratee(item))
  })
  return result
};

 

  • 빈 배열 result 생성
  • _.each를 통해 배열의 요소 순회
  • arr의 요소 item에 iteratee 함수를 적용한 뒤, 결과를 result에 push

10. _.pluck

// _.pluck은
//  1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
//  2. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
//  3. 최종적으로 새로운 배열을 리턴합니다.
// 예를 들어, 각 개인의 정보를 담은 객체를 요소로 갖는 배열을 통해서, 모든 개인의 나이만으로 구성된 별도의 배열을 만들 수 있습니다.
// 최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 합니다.
// 따라서 찾고자 하는 key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 입니다.
_.pluck = function (arr, keyOrIdx) {
  return _.map(arr, function(el){
    return el[keyOrIdx]
  })
};

 

  • _.map을 통해 새로운 배열을 리턴
  • arr의 요소 el에 적용할 함수는, el[keyOrIdx]를 리턴
  • _.map은 내부에서 빈 배열을 만들고, el에 적용된 함수의 return값을 빈 배열에 추가한 뒤 해당 배열을 리턴하는 구조
  • 따라서, el[keyOrIdx]들이 담긴 배열이 생성되고 리턴됨

11. _.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는 세 번째 인자로 초기 값을 전달받을 수 있습니다.
// 이 세 번째 인자로 초기 값이 전달되는 경우, 그 값을 누적값의 기초(acc)로 하여 배열의 '첫 번째' 요소부터 반복 작업이 수행됩니다.
// 반면 초기 값이 전달되지 않은 경우, 배열의 첫 번째 요소를 누적값의 출발로 하여 배열의 '두 번째' 요소부터 반복 작업이 수행됩니다.
_.reduce = function (arr, iteratee, initVal) {
  let acc = initVal
  _.each(arr, function(item, idx, array){
    if(initVal === undefined && idx === 0){
      acc = item
    } else {
      acc = iteratee(acc, item, idx, array)
    }
  })
  return acc
};

 

  • arr배열, iteratee함수(연산 함수), initval(초기값)를 인자로 받음
  • acc(누적값)을 initval(초기값)으로 설정
  • _.each 함수를 통해 배열의 요소 순회
  • arr의 요소 item, item의 인덱스 idx, arr 배열 array를 매개변수로 가짐
  • initval이 undefined(전달되지 않음)이고, idx === 0일 경우, acc(누적값)에 item(첫번쨰 요소)를 저장
  • ※ idx === 0 을 추가해준 이유 : idx === 0이 없을 경우 initval는 계속 undefined 상태이므로 acc는 누적값이 아니라 item이 되어버림.
  • 아닌 경우, acc에 iteratee(누적값, 현재요소, 인덱스, 배열) = 연산 결과를 저장
  • 최종 연산 결과는 acc이므로, acc를 리턴

 

이렇게 메서드들의 작동 원리와 구성들을 야물딱지게 알아보았다.

우리가 흔히 쓰는 메서드들이 사실은 그렇게 복잡하지 않은 메커니즘을 가졌다는데서 놀랐다,..,

또 한편으로는 짧더라도 매번 정의해서 쓰기는 매우 귀찮았을 것 같은데 내장 메서드로 만들어주신 JS 개발진들께 무궁한 영광을 어쩌구,..,,.

시간이 되면 고오급 메서드들도 한번 올려봐야겠다.