reduce( ) 활용법 !

DatQueue·2022년 6월 16일
3
post-thumbnail

포스팅 시작에 앞서

일전에 자바스크립트 문법 기초를 공부하면서 이 "reduce"에 관해 공부를 하긴 했지만 몇 가지 간단한 코드를 짜는데 있어서 한번도 쓰인적이 없었고 과연 꼭 배울 필요가 있을까 생각했었다. 하지만 최근 node.js 공부를 하던 도중 reduce( )를 사용한 코드를 알게 되었고 내가 알던 reduce( )의 기능과 좀 다르게 쓰일 수도 있다는 사실과 그 기능이 생각보다 강력하다는 사실을 깨달았다.

이러한 경험을 바탕으로 reduce( )에 관해 꼭 남겨보는 것이 좋다고 생각이 들어 포스팅을 간단히 적어보고자 한다. 참고로 reduce( )에 관해 세세하게 혹은 딥하게 알고 싶다면 이 글과는 무관하다는 것을 알고 봐주셧으면 한다.

Array.prototype.reduce()

간단하게 MDN 공식문서를 참고해서 reduce( )에 관해 말하자면
먼저, 구문은 다음과 같다.

arr.reduce(callback[, initialValue])
  • callback
    : 배열의 각 요소에 대해 실행할 함수, 다음 네 가지 인수를 받는다.
    • accumulator
      : 누산기는 콜백의 리턴값을 누적한다. 만약 콜백의 첫 번째 호출이면서 initialValue를 제공한 경우에는 initialValue의 값이다.
    • currentValue
      : 처리할 현재 요소.
    • currentIndex (optional)
      : 처리할 현재 요소의 인덱스. initialValue를 제공한 경우 0, 아니면 1부터 시작
    • array (optional)
      : reduce()를 호출한 배열
  • initialVlue (optional)
    : callback의 최초 호출에서 첫 번째 인수에 제공하는 값, 초기값을 제공하지 않으면 배열의 첫 번째 요소를 사용한다.

흔히 생각하는 reduce() 예시

  • 간단한 덧셈예제
const num = [1, 2, 3, 4];
const sum = num.reduce((prev, curr) => {
  return prev + curr;
});
console.log(sum);  //10

흔히 reduce( )하면 떠오르는 간단한 예시이다. 주로 return값으로 이전값(누적값)과 현재값의 덧셈을 받아오고 있고 그에 따라서 순회하는 배열 요소들의 합을 구하는 코드이다. 초기값은 설정해주지 않았으므로 디폴트 값인 0이고 만약 여기서 초기값을 1으로 지정했다면 누적합은 11이되는 구조이다.

  • object 배열을 순회하는 reduce - 덧셈예제
    이것또한 reduce()를 이용한 덧셈예제이다. 하지만 조금 다를 것이다.
const initialValue = 0;

const objectNum = [{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }];

const sum = objectNum.reduce((acc, curr) => {
  return acc + curr.x;
}, initialValue);

console.log(sum);

그냥 array의 값들을 순회하기 보단 object의 key값을 받아와 배열을 순회 후 덧셈을 하는 예제이다. 의미는 앞전 예시와 크게 벗어나지 않는다.

reduce()의 강력한 기능들 (활용)

지금부터 미처 생각하지 못했던 강력한 reduce()의 기능을 예시를 통해 적어보고자 한다. 작성자 본인처럼 reduce()덧셈 계산이 전부라고 생각했던 사람들에게 꼭 도움이 될 것이라 생각한다.

> grouping (그룹핑)

: 지금부터 볼 코드 예제는 유저의 로그인 검증 기능을 구현하는데 있어 사용될 간단한 로직이다. 백엔드 로직은 지금 구현할 수는 없으니 어떻게 동작하는지는지 정도만 이해하고 여기서 "reduce()"가 어떻게 활용되는지 , 그리고 왜 title이 grouping 인가에 중점을 맞춰 알아보자.

const users = {
  id: ["Nemoo", "yeeSiSi", "yurim"],
  psWord: ["1234", "12345", "123456"],
  name: ["DaeGyu", "SiHyun", "Yurim"],
};

const id = users.id;
const inputID = id[Math.floor(Math.random() * id.length)];

const idx = users.id.indexOf(inputID);
const usersKeys = Object.keys(users);
const userInfo = usersKeys.reduce((newUser, info) => {   // reduce
  newUser[info] = users[info][idx];
  return newUser;
}, {});
console.log(userInfo);

reduce( )를 설명하기 앞서 간단히 로직을 설명하겠다.
먼저 users라는 객체안에 id, psWord, name 값이 저장되어있다. 지금이야 저렇게 직접 값을 적어줬지만 실제 프로젝트일 경우 당연히 DB를 통해 불러올 것이다.

그리고 user의 id값(value)을 id로 받아오고 지금 일일히 클라이언트의 로그인 입력값을 받아오기엔 구현해야 할 로직이 너무 많아서 (지금은 reduce 설명이 우선이니까) 저렇게 inputID를 랜덤하게 받아오기로 하였다.

우리는 해당 코드를 통해 일정 id값에 따른 같은 인덱스에 위치하는 psWord와 name을 포함하는 object받고자한다. 그에 따라 그 index를 결정하기 위해 indexOf()을 이용해서 랜덤하게 받아온 id값의 index를 idx 변수에 넣어준다.

이제 reduce( )구문을 해석할 건데 앞서 reduce는 array를 순회한다. 즉 array 가 필요할 것이다. 그러므로 users 객체의 key값인 id, psWord, name을 array로 만들어준다. ( Object.keys()이용 )

usersKeys = ["id","psWord","name"]

이제 reduce( )를 파헤쳐보자.

먼저 콜백의 파라미터이다.
accumulator(누적값)로 newUser를 받고, currentValue(현재값)로 info를 받게한다.
첫번째 순환에서는 acc인 newUser는 초깃값이 된다. 즉 빈 object인 "{ }"이다. 그리고 현재값 info는 "id"일 것이므로

newUser[info] = users[info][idx];

이 코드를 통하여 newUser["id"] = users["id"][idx]와 같이 처리될 것이다.
즉, 만약 랜덤하게 받아온 id값이 "Nemoo"라면 첫 번째 순환에선 리턴값으로

{id : "Nemoo"}

를 가지게 될 것이다. 이것이 리턴값인 newUser인 것이고 즉 리턴값 newUser은 다시 acc부분 newUser 파라미터로가 userKeys 배열을 다 순회할까지 반복한다.

그럼 두 번째 순환에서 리턴 값 newUser은

{id : "Nemoo", psWord : "1234"}

를 가지게 되고, 마지막 세 번째 순환에서 최종 리턴 값은

{id : "Nemoo", psWord : "1234" , name : "DaeGyu"}

를 가지게 된다. 콘솔 창에 찍어보면 동일하게



결과를 얻을 수 있다.

우린 이처럼 reduce()를 이용해 arrayobject형태로 필요한 요소만을 grouping 할 수 있게 된 것이다.

> Counting (카운팅)

const users = {
  id: ["Nemoo", "Nemoo", "Nemoo", "Yurim", "Yurim", "John"],
};

const id = users.id;

let countedNames = id.reduce((allNames, name) => {
  if (name in allNames) {
    allNames[name]++;
  } else {
    allNames[name] = 1;
  }
  return allNames;
}, {});

console.log(countedNames);

다음과 같이 중복된 이름의 아이디가 존재하는 id의 값에 해당하는 array가 있다.
코드를 해석해보자.

만약, 누적값 allNames에 현재값 name이 포함되있다면 value값을 1씩 증가시키고 그렇지않다면 1을 value값으로 가지는 object를 만들게 된다.

이 코드는 if 내부보다는 else 내부를 먼저 고려해야하는데 처음에는 초기값인 빈 object "{ }"에 첫 번째 요소인 "Nemoo"가 key값으로 들어가게 되고 그때 value값은 1이 될 것이다.

else {
    allNames[name] = 1;
  }

좀 더 쉽게 말하자면 처음 순환에는 allNames(빈 오브젝트)안에 들어가게 될 "Nemoo"요소가 하나밖에 없고 즉, 현재값 name이 allNames에 포함되지 않은 상태이므로

{Nemoo : 1}

다음과 같은 오브젝트를 얻게 되고 return allNames를 통해 다시 reduce의 누적값으로 들어가 순환을 시작한다.

두 번째 순환에는 "Nemoo" 요소가 이미 오브젝트에 포함되어 있으므로

if (name in allNames) {
    allNames[name]++;
  } 

다음 코드에 의해 기존 Nemoo의 value값 1에서 1증가한 2가 되는 것이다.

{Nemoo : 2}

이런식으로 reduce()를 전부 순환하면서

다음과 같은 object를 얻게 된다.

> Removing duplicated items (중복 항목 제거)

앞서 Counting 코드의 연장선 상에서 생각할 수 있는데 만약 유저가 회원가입을 하는 데 있어 이미 다른 유저가 등록한 중복된 아이디를 입력할 경우 해당 id값을 가질 수 없도록 취해 주어야 할 것이다.

물론 해당 로직은 서버와 DB를 통하여 진행하겠지만 일단 그것이 이번 포스팅의 중점은 아니므로 간단히 의미정도만 이해하면 되겠다.

그럼 간단한 reduce()를 이용한 로직을 통해 중복 항목 제거를 구현해보자.

  • solve 1 )
const users = {
  id: ["Nemoo", "Nemoo", "Nemoo", "Yurim", "Yurim", "John"],
};

const id = users.id;

let removeDuplicated = id.reduce((allNames, name, index) => {
  if (allNames.includes(name)) {
    delete allNames[index];
  } else {
    allNames[index] = name;
  }
  return allNames;
}, []);

const newArr = removeDuplicated.filter((e) => {
  return e !== "";
});

console.log(newArr); // ['Nemoo', 'Yurim', 'John']

첫 번째 로직은 작성자 본인이 가장 먼저 작성해보았던 중복 항목 제거 로직이다. reduce()부분과 fillter()부분으로 나누어져있다.

reduce 부분을 해석하자면 카운팅 부분과 마찬가지로 allNamesname를 포함한다면 중복항목을 제거하고 그렇지 않다면 name을 allNames배열에 포함시켜주는 식으로 진행하였다.

이때,

delete allNames[index];

다음과 같이 delete를 이용해 현재 index에 대한 배열의 요소값을 제거하도록 하였다.
이렇게 reduce() 로직만 구현한 뒤 console창에서 출력한 결과 원하는 결과인 중복 항목 제거에 성공한 줄 알았지만 다음과 같이

배열에 중복항목들이 index를 가지고 있는 empty값으로 출력되게 되었다.
empty가 되면서 중복항목들이 제거가 된 것은 맞지만 인덱스를 가지는 배열 요소로써 포함되므로 완전히 중복항목들이 제거되었다고 할 수는 없다.

그렇다면 왜 다음과 같이 empty가 나오게 된 것일까?

delete를 어원 그대로 해석해서 배열요소를 그냥 삭재해준다고 생각할 수도 있지만 delete로 배열의 요소를 제거할 시 배열의 길이는 그대로이게 된다.

즉, delete연산자는 배열 요소의 삭제가 아닌 빈 값으로 변경하기 때문에 삭제보다는 변경에 가까운 개념이다.

결국 filter 구문을 추가함으로써 empty부분을 제거해 완전한 중복항목 제거된 배열을 생성할 수 있었다.

  • solve2 )
const users = {
  id: ["Nemoo", "Nemoo", "Nemoo", "Yurim", "Yurim", "John"],
};

const id = users.id;

let removeDuplicated = id.reduce((allNames, name, index) => {
  if (allNames.indexOf(name) === -1) {
    allNames.push(name);
  }
  return allNames;
}, []);

console.log(removeDuplicated); //['Nemoo', 'Yurim', 'John']

이번에는 reduce 만으로 중복요소 제거를 성공한 코드이다. indexOf를 사용해 allNames안에 name이 없을 때 (index ==== -1일 때) push를 사용해 allNames안에 name을 넣어주도록 코드를 작성하였다.

포스팅을 마치며 ...

이번 포스팅에선 자바스크립트의 메서드인 reduce가 어떻게 유용하게 쓰일 수 있는가, 어떤 기능을 할 수 있는 가에 대해 아주 간단한 예시를 통해 알아보았다.

처음 reduce를 접하였을 때 누적값, 현재값을 이용한 덧셈예시를 가장 먼저 볼 수 있었고, 어느 강의들이나 , 어느 블로그들이나 reduce에 대해 설명할 때는 항상 덧셈예시 위주로 설명했던 것 같다.
그로 인해 reduce의 좋은 기능에 대해 알지 못하였고 이런 메서드에 대해 굳이 필요한가에 대한 의문만 쌓여갔었다.

그때, node.js 강좌를 듣던 중 reduce를 이용해 원하는 배열을 추출하는 과정을 접하게 되었고 reduce가 강력한 메서드란 말이 괜히 언급되는게 아니구나... 이런 점에서 하는 말이었구나... 하는 생각이 들었다.
정말 메서드란 사용하기 나름인 것 같다.

다음 포스팅에서 간단하게 회원가입 절차에서 reduce를 활용한 예시를 들어 조금 더 실용적으로 다가올 수 있게끔 구현해보겠다.

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

0개의 댓글