코딩/Javascript

자바스크립트 Javascript map, filter, reduce 동작 원리 완전분석

드리프트 2022. 4. 4. 20:52
728x170

 

안녕하세요?

 

오늘은 자바스크립트에서 가장 많이 쓰이는 객체인 배열의 연산 관련 함수인 map, filter, reduce에 대해 알아보겠습니다.

 

React로 웹 프로그래밍할 때 가장 많이 쓰는 게 아마 map 함수 일 건 대요.

 

자바스크립트는 배열을 이용하여 쉽게 데이터를 가공할 수 있게 하는 헬퍼 유틸을 많이 가지고 있습니다.

 

위의 첫 그림을 보시면 find, findIndex, indexof 같은 함수는 딱 봐도 쉽게 이해할 수 있는데요.

 

오늘은 자바스크립트 코딩 초창기에 느꼈을 법한 map, filter, reduce에 대해 아주 쉽게 알아보겠습니다.

 

 

Map

 

먼저, map 함수입니다.

MDN에 나와있는 문법인데요.

 

읽어보면 너무 어렵습니다.

 

map함수를 쉽게 이해하려면 예제를 들어서 이해하면 쉽습니다.

 

const arr = [5, 1, 3, 2, 6];

// double
// [10, 2, 6, 4, 12]

// triple
// [15, 3, 9, 6, 18]

// binary
// ["101, "1", "11", "10", "110"]

arr 이란 배열이 있습니다.

 

이 배열의 각 숫자를 곱하기 2(double), 곱하기 3(triple), 이진법 표시하기 등의 작업을 한다고 할 때 일일이 arr [0], arr [1], arr [2] 이런 식으로 수작업할 거면 자바스크립트를 쓰는 의미가 없는데요.

 

우리가 기억해둬야 할 거는 map 함수는 배열 속의 각각의 원소에 대해 어떤 특정한 작업을 할 때 쓰는 함수입니다.

 

map 함수의 문법은 다음과 같습니다.

 

const output = arr.map(function);

 

arr.map에 작동원리가 되는 함수를 넣어주면 새로운 배열을 뽑아내는데 그 배열이 output으로 저장된다는 뜻입니다.

 

그럼, function 부분에 우리가 원하는 작동원리를 넣어주면 되는데요.

 

곱하기 2, 곱하기 3, 이진법 표시하기 등의 작동원리를 넣어주면 됩니다.

 

한번 작성해 볼까요?

 

const arr = [5, 1, 3, 2, 6];

// double
// [10, 2, 6, 4, 12]

// triple
// [15, 3, 9, 6, 18]

// binary
// ["101, "1", "11", "10", "110"]

function double(x) {
  return x * 2;
}

function triple(x) {
  return x * 3;
}

function binary(x) {
  return x.toString("2");
}

const output = arr.map(double);

const output2 = arr.map(triple);

const output3 = arr.map(binary);

console.log(output);
console.log(output2);
console.log(output3);

원하는 작동원리의 함수를 각각 double, triple, binary라는 이름으로 작성하고 그 함수를 arr.map() 안에 넣어줬습니다.

 

작동 결과를 볼까요?

 

우리가 원하는 결과로 나왔습니다.

 

그런데, 꼭 map() 안에 들어가는 함수를 외부에 따로 정의해야 될까요?

 

아닙니다.

 

double함수를 예로 들어 볼까요?

 

// function double(x) {
//   return x * 2;
// }

const output = arr.map(function double(x) {
  return x * 2;
});

외부에 작성한 double 함수를 map() 안에 통째로 넣었습니다.

 

결과는 똑같이 작동하고요.

 

그럼 여기서 좀 더 멋지게 ES6의 애로우 함수로 바꿔 볼까요?

 

const output = arr.map((x) => {
  return x * 2;
});

() => {} 방식인데요. 위와 같은 경우 map() 안에 있는 함수를 익명 함수(anonymous function)라고 합니다.

 

만약에 익명 함수가 한 줄이면 다음과 같이 변경할 수 있습니다.

 

const output = arr.map((x) => x * 2);

(x) => x * 2라고 return을 생략할 수 있습니다.

 

이제, map 함수에 대해서 위와 같이 애로우 함수 형태를 잘 기억해야 합니다.

 

React 코드에서 정말 많이 쓰이거든요.

 

참고로 React 코드에서 map을 쓸 때 index를 쓰는 방법이 있습니다.

 

map() 안에 위치하는 함수에 두 개의 인자를 쓸 때 두 번째 인자가 바로 index 역할을 하는데요.

 

위와 같이 출력하고자 한다면 바로 다음과 같이 짜면 됩니다.

 

const arr = [5, 1, 3, 2, 6];

const output = arr.map((x, i) => console.log("index ", i, " : ", x * 2));

map() 안에 위치하는 익명 함수의 첫 번째 인자는 iterate 하는 인자이고, 여기서는 x라고 이름 지었습니다.

 

그리고 두 번째로 위치한 인자는 index 역할을 하고 여기서는 i라고 이름 지었습니다.

 

React 코드에서 map을 할 때는 꼭 key 값을 넣으라고 하잖아요.

 

이때 map의 index 값을 넣으면 됩니다. 참고 바랍니다.

 

 

 

Filter

 

두 번째는 filter 함수입니다.

 

filter함수는 React 코드에서 DB의 자료를 가지고 왔을 때 특정 조건에 해당되는 것만 추려낼 때 쓰는 아주 유용한 함수인데요.

 

일단 filter 함수의 문법도 뭔가 어려워 보이는데요.

 

예를 들어 생각해 보겠습니다.

 

const arr = [5, 1, 3, 2, 6];

아까 예로 든 arr 배열에서 홀수만 뽑아내고 싶을 때 쓰는 함수가 바로 filter함수입니다.

 

const arr = [5, 1, 3, 2, 6];

// odd number
function isOdd(x) {
  if (x % 2) {
    return true;
  } else return false;
}

const output = arr.filter(isOdd);

console.log(output);

 

filter() 안에 들어가는 함수는 무조건 true, false를 리턴하는 함수여야 하는데요.

 

위 코드에서 보면 isOdd 함수에서 return true와 return false를 각각 리턴하고 있습니다.

 

위의 코드를 좀 더 간단히 해볼까요?

 

const arr = [5, 1, 3, 2, 6];

const output = arr.filter((x) => x % 2);

console.log(output);

작동 결과는 똑같습니다.

 

애로우 함수로 정말 간단하게 줄였는데요.

 

x % 2 로직은 짝수면 0을 리턴하기 때문에 0은 자바스크립트에서는 false가 됩니다.

 

그래서 홀수만 뽑아내는 filter() 함수가 되는데요.

 

그럼 짝수만 뽑아내는 filter() 함수는 어떻게 만들까요?

 

const arr = [5, 1, 3, 2, 6];

const output = arr.filter((x) => x % 2 === 0);

console.log(output);

x % 2 === 0이라고 아주 강력하게 조건식을 부여해서 짝수만 필터링하는 함수를 만들었습니다.

 

그럼, 4보다 큰 숫자는 어떻게 구현할까요?

 

const arr = [5, 1, 3, 2, 6];

const output = arr.filter((x) => x > 4);

console.log(output);

 

정말 쉽지 않나요?

 

filter() 함수는 한 가지만 명심하시면 됩니다.

 

filter() 함수 안에 들어갈 익명 함수는 true와 false를 리턴하는데, true인 것만 뽑아내는 함수다라고 이해하시면 됩니다.

 

true와 false만 리턴한다고 했는데 사실은 0과 0 이외의 숫자만 리턴해도 상관없습니다.

 

자바스크립트에서는 0은 false를 뜻하고 0 이외의 숫자는 true로 인식하기 때문입니다.

 

filter함수를 이용한 좀 더 어려운 예를 들어 볼까요?

 

다음 예제를 들어 설명해 보겠습니다.

 

let employees = [
  { id: 20, name: "Kim", salary: 30000, dept: "it" },
  { id: 24, name: "Park", salary: 35000, dept: "hr" },
  { id: 56, name: "Son", salary: 32000, dept: "it" },
  { id: 88, name: "Lee", salary: 38000, dept: "hr" },
];

 

employees 배열이 있고 각각의 요소는 객체인데 직원의 성과 월급, 부서를 포함하고 있습니다.

 

여기서 부서가 it 쪽인 사람만 필터링하고 싶다고 가정한다면 어떻게 해야 할까요?

 

let employees = [
  { id: 20, name: "Kim", salary: 30000, dept: "it" },
  { id: 24, name: "Park", salary: 35000, dept: "hr" },
  { id: 56, name: "Son", salary: 32000, dept: "it" },
  { id: 88, name: "Lee", salary: 38000, dept: "hr" },
];

let departmentList = employees.filter(function (record) {
  return record.dept == "it";
});
console.log(departmentList);

filter() 함수 안에 익명 함수에 return 되는 항목을 잘 보시면 record 가 iterate 되는 변수이고 이 변수의 dept 항목이 "it"인 경우만 비교해서 true면 그 객체를 리턴하고 false면 그 객체를 리턴하지 않습니다.

 

그래서 결과는 위와 같이 dept : 'it'인 사람만 추려지는 거죠.

 

filter() 함수는 여기까지만 이해해도 충분할 것으로 생각됩니다.

 

 

Reduce

 

마지막으로 가장 어려운 reduce 함수인데요.

 

 

reduce가 제가 배운 배열 함수 중에 가장 어려웠습니다.

 

reduce함수는 map, filter처럼 일정한 규칙이 아니고 완전히 새로운 규칙을 적용해서 배열을 뽑아낼 때 사용하는데요.

 

예를 들어 다음과 같은 배열이 있다고 합시다.

 

const arr = [5, 1, 3, 2, 6];

이 배열의 인자 전체를 합친 값은 얼마인가라는 함수를 만든다고 생각해 볼까요?

 

for 루프를 돌리면 쉽게 구할 수 있는데요.

 

const arr = [5, 1, 3, 2, 6];

// sum
function findSum(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum = sum + arr[i];
  }
  return sum;
}

console.log(findSum(arr));

위 코드에서 fundSum 함수는 배열의 인자를 전부 더한 합계 값을 리턴하는 함수입니다.

 

당연히 결과는 17이겠죠.

 

reduce 함수는 위와 같은 경우에 사용할 수 있는 배열의 함수입니다.

 

const output = arr.reduce(function (acc, curr) {
  acc = acc + curr;
  return acc;
}, 0);

console.log(output);

reduce() 함수는 그 인자로 익명 함수 한 개와 초기값을 요구합니다.

 

위 코드에서 보면 익명 함수 다음에 0이라는 초기값을 부여했는데요.

 

그리고 익명 함수의 인자는 두 개입니다.

 

첫 번째 인자는 누적 인자라고 하고 accumulator라고 부릅니다.

 

위에서는 acc라는 이름으로 지정했습니다.

 

그리고 두 번째는 현재 인자라고 하는 currentValue인데요.

 

위에서는 curr이라고 이름 지었습니다.

 

코드를 잘 보시면 배열의 전체 합산을 하기 위해 누적 인자 acc에 curr을 계속 더하게 되는데요.

 

curr이 바로 배열이 iterate 될 때의 그 값이 됩니다.

 

즉, arr 배열이 5개의 값을 가지고 있기 때문에 총 5번 iterate 되는데요. 쉽게 말해 5번 도는데요.

 

첫 번째 돌 때 curr 값이 5, 두 번째 돌 때는 1, 세 번째 돌 때는 3 등등으로 진행됩니다.

 

그리고 acc라는 누적 인자는 reduce(익명 함수, 초기값) 형식에서 초기값을 맨 처음 지정받아서 그다음에 누적되는 변수인데요.

 

초기값을 0이라고 지정했고 acc = acc + curr이라는 코드에 의해 계속 배열의 항목 값을 더하게 됩니다.

 

그리고 다 끝나면 누적 인자인 acc를 리턴하게 됩니다.

 

 

결과는 똑같은데요.

 

이번에는 reduce 함수 안에 애로우 함수를 써볼까요?

 

const output = arr.reduce((acc, curr) => {
  acc = acc + curr;
  return acc;
}, 0);

애로우 함수라고 어려운 게 아니라 위와 같이 사실은 별거 없습니다.

 

앞으로 React 코딩할 때 애로우 함수를 가장 쓰게 되니까 애로우 함수에 익숙해지는 것도 좋습니다.

 

reduce의 두 번째 예제인데요.

 

배열에서 가장 큰 값을 구하는 max 함수를 구현해 볼까 합니다.

 

일반적인 코드로는 아래와 같이 구현하면 됩니다.

 

const arr = [5, 1, 3, 2, 6];

// sum
function findMax(arr) {
  let max = 0;
  for (let i = 0; i < arr.length; i++) {
      if (arr[i] > max) {
          max = arr[i];
      }
  }
  return max;
}

console.log(findMax(arr));

 

위의 findMax 함수를 reduce함수로 구현해 보겠습니다.

 

방법은 아까와 비슷합니다.

 

const output = arr.reduce(function (acc, curr) {
  if (curr > acc) {
    acc = curr;
  }
  return acc;
}, 0);

console.log(output);

결과는 똑같습니다.

 

reduce의 다른 예를 들어 보겠습니다.

 

let employees = [
  { id: 20, name: "Kim", salary: 30000, dept: "it" },
  { id: 24, name: "Park", salary: 35000, dept: "hr" },
  { id: 56, name: "Son", salary: 32000, dept: "it" },
  { id: 88, name: "Lee", salary: 38000, dept: "hr" },
];

위와 같은 배열이 있다고 했을 때 부서가 it인 부서는 몇 명일까를 계산해 볼까요?

 

이 걸 계산하기 전에 먼저 자바스크립트의 객체를 참조하는 방법을 배워볼까 합니다.

 

다음과 같은 객체가 있다고 가정하면

const a = { 1: 20, name: "Kim", salary: 30000, dept: "it" };

보통 객체. 항목 방식으로 많이들 이 들하고 계신데요.

 

객체. 항목 방식은 a.name과 a.salary 그리고 a.dept 와 같이 사용할 수 있습니다.

 

즉, 객체.항목 방식은 객체의 항목 이름이 숫자가 아닐 경우에 사용하는데요.

 

위의 객체 1 : 20에 대해서는 어떻게 참조할까요?

 

바로 a [1]과 같이 참조하면 됩니다. 즉, 객체[항목] 방식인데요.

 

그러면 name과 salary, dept는 어떻게 참조할까요?

 

아래와 같이 따옴표("")를 사용하면 됩니다.

const a = { 1: 20, name: "Kim", salary: 30000, dept: "it" };

console.log(a[1]]);
console.log(a["name"]);
console.log(a["salary"]);
console.log(a["dept"]);

 

자바스크립트 객체의 참조 방법에 대한 설명이 끝났으니까 원래의 reduce 함수 예제로 돌어가서 dept가 it인 개수를 세고 싶을 때는 어떻게 해야 하는지 알아볼까요?

 

const output = employees.reduce(function (acc, curr) {
  if (acc[curr.dept]) {
    acc[curr.dept] = ++acc[curr.dept];
  } else {
    acc[curr.dept] = 1;
  }
  return acc;
}, {});

console.log(output);

reduce() 함수 안의 익명 함수가 있고 두 번째 인자로 초기값을 {}라고 빈 객체를 줬습니다.

 

그러면 누적 인자인 acc는 처음에 빈 객체가 되는데요.

 

acc = {} 상태에서 iterate를 돌려 볼까요?

 

acc [curr.dept]는 어떻게 해석해야 될까요?

 

바로 acc ["it"]라고 해석될 수 있는데요.

 

즉, if (acc ["it"])라는 if문에서는 당연히 false가 나오니까 els문에서 acc ["it"] 값에 1을 배당하게 됩니다.

 

즉, reduce 가 한번 루프를 돌렸을 때 리턴되는 acc는 객체인데 그 객체 안의 내용이 바로 { it : 1} 이 되는 거죠.

 

curr이 두 번째가 됐을 때는 acc ["hr"] 이기 때문에 방금 루프 한번 돌렸을 때의 리턴 값이자 누적 값인 { it: 1}에서 "hr"에 해당되는 항목이 없기 때문에 두 번째 루프도 if문은 false가 되어 acc ["hr"] = 1 이 돌려지게 됩니다.

 

근데 acc는 누적 인자 값이기 때문에 초기값이 객체이고 누적되기 때문에 { it : 1, hr : 1 }이라는 형태로 리턴되죠.

 

reduce의 세 번째 루프를 돌려볼까요?

 

세 번째는 if 문에서 if(acc ["it"])라는 조건식에 의해 2번째 루프에서 리턴된 acc의 값은 { it : 1, hr : 1 }이 되고 여기서 acc ["it"] 값은 1이 되기 때문에 이번에는 if문이 true가 됩니다.

 

true가 되면 acc [curr.dept] = ++acc [curr.dept]에 의해 1이 증가되게 되는 거죠.

그래서 최종적으로 루프를 돌리면 다음과 같이 결과값이 나오게 됩니다.
 

{ it : 3, hr : 2 }처럼 it 부서는 3명, hr 부서는 2명이라는 값이 나오는 겁니다.

 

이번 예제를 조금 어려운데요.

 

reduce함수는 그 활용방안이 무궁 무궁하기 때문에 완벽히 숙지하면 자바스크립트 코딩에 많은 도움이 될 겁니다.

 

그럼.

그리드형