[TIL] reduce() 메서드가 NaN을 반환하는 이유

July 09, 2021

프로그래머스에서 문제를 푸는 데 reduce 메서드가 왜 NaN을 리턴하는지 도무지 원인을 찾을 수 없었다.

한참을 삽질한 끝에 내가 reduce 메서드를 크게 잘못 이해하고 있었다는 것을 알 게 됐다. 그동안은 reduce를 사용하여 주로 숫자만 더해왔기 때문에 내가 잘못 알고 있었다는 사실 조차 모르고 있었는데, 이 문제에서는 reduce 메서드의 콜백 함수에서 숫자가 아니라 객체를 참조해야 했기 때문에 제대로 된 답을 구할 수 없었다.

잘못 사용한 reduce의 예시

const map = {
    // '음악 장르' : [[곡 고유번호, 곡 재생시간]]
    'classic' : [[3, 800], [0, 500], [2, 150]],
    'pop' : [[4, 2500], [1, 600]]
};

let totalPlayTime = [];
for(let genre in map) {
    let timeSum = map[genre].reduce((a, b) => a[1] + b[1]);
    totalPlayTime.push(timeSum);
}

console.log(totalPlayTime); // [NaN, 3100]

각 장르에 속한 모든 곡들의 재생시간의 총합을 구하기 위해 reduce 메서드를 사용하였고,

reduce를 호출한 map[genre] 배열의 요소는 [곡 고유번호, 곡 재생시간]으로 구성된 이중 배열 형태이니 배열의 1번 인덱스의 값들만 더하기 위해 a[1] + b[1]을 리턴하도록 했다.

문제는 ‘classic’의 경우 NaN을 반환하지만, ‘pop’은 제대로 된 합산 결과인 3100을 반환한다는 것이었다.

나는 sort() 메서드처럼, reduce 콜백함수의 인수 또한 순회하는 요소를 가리킨다고 생각했다. 그도 그럴 것이 그동안은 주로 reduce를 사용하여 숫자 타입의 값만 담긴 배열을 계산했기 때문에 문제되는 경우가 없었다.

그러나 내가 위의 풀이에서 사용한 reduce 콜백함수의 (a, b)는 map[genre]의 요소인 [3, 800], [0, 500], [2, 150]을 뜻하는 것이 아니다.

왜 배열의 1번 인덱스의 값을 더하는 800 + 500 + 150이 아닌 NaN이 반환되었을까.

reduce 제대로 알기

reduce() 메서드는 배열의 각 요소에 대해 주어진 reducer 함수를 실행하고, 하나의 결과값을 반환한다.

이때, reducer 함수는 네 개의 인자를 갖는다.

  1. 누산기 (accumulator)
  2. 현재 값
  3. 현재 인덱스
  4. 원본 배열

내가 잘못 알고 있었던 부분은 바로 첫 번째 인자인 accumulator였다. 위의 풀이에서 ‘a’ 파라미터에 해당되는 부분이다.

accumulator는 초기값 또는 이전 반환값(즉, 반드시 return이 필요하다)을 가리킨다. 여태까지 주로 요소가 전부 숫자인 배열에 reduce를 사용하여 값을 더해왔기 때문에 이전 반환값 역시 숫자였고, 숫자 타입인 이전 반환값(a)에 또다시 숫자 타입인 현재 값(b)를 더했기 때문에 제대로된 숫자 값을 반환한 것이다.

그러나 이번에는 경우가 달랐다. 내 풀이는 다음과 같은 순서로 진행된다.

  1. 초기값을 생략했기 때문에 첫 번째 a는 map[genre]의 첫 번째 요소인 배열 [3, 800]이다.
  2. 첫 번째 b는 map[genre]의 두 번째 요소인 배열 [0, 500]이다.
  3. 따라서 a[1] + b[1] 은 800 + 500 = 1300이 된다.
  4. 이 때, 1300이 이전 반환값이다. 따라서 두 번째 순회의 a는 배열이 아닌 숫자 값 1300이다.
  5. 두 번째 b는 [2, 150]이다.
  6. 따라서 두 번째 a[1] + b[1]은 1300[1] + [2, 150][1]이다. 1300[1]은 undefined이기 때문에 최종 반환 값은 undefined + 150 = NaN이다.

이처럼 reducer 함수의 인수가 무엇을 의미하는지 정확하게 알고 있지 못했기 때문에 NaN이 반환되었던 것이다.

따라서 정확한 풀이는 다음과 같아야 할 것이다.

const map = {
    // '음악 장르' : [[곡 고유번호, 곡 재생시간]]
    'classic' : [[3, 800], [0, 500], [2, 150]],
    'pop' : [[4, 2500], [1, 600]]
};

let totalPlayTime = [];
for(let genre in map) {
    let timeSum = map[genre].reduce((acc, cur) => acc + cur[1], 0);
    totalPlayTime.push(timeSum);
}

console.log(totalPlayTime); // [1450, 3100]
  1. 초기값은 0이다.
  2. 첫 번째 cur는 [3, 800]이다. 따라서 acc + cur[1]은 0 + 800 = 800이다.
  3. 두 번째 acc는 이전 반환값인 800이다. cur는 [0, 500]이기 때문에 acc + cur[1]은 1300이다.
  4. 세 번째 acc + cur[1]은 1300 + 150 = 1450이다.

이제 NaN이 아닌 원했던 값을 제대로 반환한다.

오늘의 교훈

reduce를 사용할 때 가급적 초기값을 설정하는 것을 잊지말자.
a, b 같은 의미없는 변수보다는 acc, cur 처럼 이해하기 좋은 변수명을 사용하자.
메서드를 정확하게 이해하고 사용하자.


Profile picture

42KIM A person trying to create something.
👉Github