고차함수 (filter, map, reduce)

왕지호·2022년 11월 17일
0

오늘은 함수 중 고차함수에 대해 알아보자!

먼저, 고차함수를 논하기 전 함수는 무슨 객체인지 알아보자!

일급 객체

  • 대표적인 일급 객체 (first-class citizen)중 하나
  • JavaScript에서 함수는 아래와 같이 특별하게 취급한다
    • 변수에 할당(assignment) 할 수 있다.
    • 다른 함수의 전달인자(argument)로 전달될 수 있다.
    • 다른 함수의 결과로서 리턴될 수 있다.
  • 함수를 변수에 할당할 수 있기 때문에, 함수를 배열의 요소나 객체의 속성값으로 저장할 수 있다
  • 함수를 데이터(string, number, boolean, array, object)처럼 다룰 수 있다

그렇다면 고차함수는 무엇일까?

고차함수

"함수는 변수에 저장할 수 있습니다. 그리고 함수는, 함수를 담은 변수를 전달인자로 받을 수 있습니다. 마찬가지로, 함수 내부에서 변수에 함수를 할당할 수 있습니다. 그리고 함수는 이 변수를 리턴할 수 있습니다."

  • 고차함수는 변수에 할당하지 않고 함수를 바로 이용할 수 있다.
  • 고차 함수(higher order function)는 함수를 전달인자(argument)로 받을 수 있고, 함수를 리턴할 수 있는 함수이다.

예를 들어 어떤 고차 함수에 함수를 전달인자로 전달한다고 하자. 그럼 고차 함수는 함수 자체를 리턴한다. 변수가 빠졌을 뿐, 동일하게 동작하는 것이다.

콜백함수

  • 다른 함수(caller)의 전달인자(argument)로 전달되는 함수이다
  • 콜백 함수를 전달받은 고차 함수(caller)는, 함수 내부에서 이 콜백 함수를 호출(invoke)할 수 있고, 조건에 따라 콜백 함수의 실행 여부를 결정할 수도 있다
  • 아예 호출하지 않을 수도 있고, 여러 번 실행할 수도 있다

커링함수

  • '함수를 리턴하는 함수’
  • 고차 함수라는 용어를 '함수를 전달인자로 받는 함수'에만 한정해 사용한다
  • 정확하게 구분하자면, 고차 함수가 커링 함수를 포함하는 것!

그렇다면 위에서 언급한 특별 취급 3가지에 대해 더 알아보자!

변수에 함수를 할당하는 경우

/*
 * 아래는 변수 square에 함수를 할당하는 함수 표현식입니다.
 * JavaScript에서 함수는 일급 객체이기 때문에 변수에 할당할 수 있습니다.
 *
 * 함수 표현식은 할당 전에 사용할 수 없습니다.
 * square(7); // --> ReferenceError: Can't find variable: square
 */

const square = function (num) {
  return num * num;
};

// 변수 square에는 함수가 할당되어 있으므로 (일급 객체), 함수 호출 연산자 '()'를 사용할 수 있습니다.
output = square(7);
console.log(output); // --> 49
  • 함수는 변수에 저장된 데이터를 전달 인자로 받거나, 리턴 값으로 사용할 수 있다
  • 함수도 변수에 저장될 수 있기 때문에 함수를 인자로 받거나, 리턴 값으로 사용할 수 있다

다른 함수를 인자로 받는 경우

 function double(num) {
   return num * 2;
 }
 
 function doubleNum(func, num) {
   return func(num);
 }
 
 /*
  * 함수 doubleNum은 다른 함수를 인자로 받는 고차 함수입니다.
  * 함수 doubleNum의 첫 번째 인자 func에 함수가 들어올 경우
  * 함수 func는 함수 doubleNum의 콜백 함수입니다.
  * 아래와 같은 경우, 함수 double은 함수 doubleNum의 콜백 함수입니다.
  */
 let output = doubleNum(double, 4);
 console.log(output); // -> 8

함수를 리턴하는 경우

 function adder(added) {
   return function (num) {
     return num + added;
   };
 }
 
 /*
  * 함수 adder는 다른 함수를 리턴하는 고차 함수입니다.
  * adder는 인자 한 개를 입력받아서 함수(익명 함수)를 리턴합니다.
  * 리턴되는 익명 함수는 인자 한 개를 받아서 added와 더한 값을 리턴합니다.
  */
 
 // adder(5)는 함수이므로 함수 호출 연산자 '()'를 사용할 수 있습니다.
 let output = adder(5)(3); // -> 8
 console.log(output); // -> 8
 
 // adder가 리턴하는 함수를 변수에 저장할 수 있습니다.
 // javascript에서 함수는 일급 객체이기 때문입니다.
 const add3 = adder(3);
 output = add3(2);
 console.log(output); // -> 5

함수를 인자로 받고, 함수를 리턴하는 경우

 function double(num) {
   return num * 2;
 }
 
 function doubleAdder(added, func) {
   const doubled = func(added);
   return function (num) {
     return num + doubled;
   };
 }
 
 /*
  * 함수 doubleAdder는 고차 함수입니다.
  * 함수 doubleAdder의 인자 func는 함수 doubleAdder의 콜백 함수입니다.
  * 함수 double은 함수 doubleAdder의 콜백으로 전달되었습니다.
  */
 
 // doubleAdder(5, double)는 함수이므로 함수 호출 기호 '()'를 사용할 수 있습니다.
 doubleAdder(5, double)(3); // -> 13
 
 // doubleAdder가 리턴하는 함수를 변수에 저장할 수 있습니다. (일급 객체)
 const addTwice3 = doubleAdder(3, double);
 addTwice3(2); // --> 8

이제 내장 고차함수에 대해 알아보자!

내장 고차함수

대표적인 내장 고차 함수로는 filter, map, reduce 등이 있다.

filter

  • 모든 배열의 요소 중에서 특정 조건을 만족하는 요소를 걸러내는 메서드이다.
  • 예를 들어 number 타입을 요소로 갖는 배열에서 짝수만을 걸러내거나, 18 보다 작은 수만을 걸러낸다.
  • string 타입을 요소로 갖는 배열에서, 길이가 10 이하인 문자열만 걸러내거나, 'korea' 같은 특정 문자열만 걸러낼 수도 있다.
    // 아래 코드에서 '짝수'와 '길이 5 이하'는 문법 오류(syntax error)에 해당합니다.
    // 의미만 이해해도 충분합니다.
    let arr = [1, 2, 3, 4];
    let output = arr.filter(짝수);
    console.log(output); // ->> [2, 4]
    
    arr = ['hello', 'code', 'states', 'happy', 'hacking'];
    output = arr.filter(길이 5 이하)
    console.log(output); // ->> ['hello', 'code', 'happy']

여기서 걸러내는 기준이 되는 특정 조건은 filter 메서드의 전달인자로 전달되는데, 이때 전달되는 조건은 함수의 형태이다.

  • filter 메서드는, 걸러내기 위한 조건을 명시한 함수를 전달인자로 받기 때문에 고차 함수이다.
  • filter 메서드가 동작하는 방식을 조금 더 자세히 살펴보면 다음과 같다
  // 아래 코드는 정확한 표현 방식은 아닙니다.
  // 의미만 이해해도 충분합니다.
    
    let arr = [1, 2, 3];
    // 배열의 filter 메서드는 함수를 전달인자로 받는 고차 함수입니다.
    // arr.filter를 실행하면 내부적으로 arr에 접근할 수 있다고 생각해도 됩니다.
    arr.filter = function (arr, func) {
      const newArr = [];
      for (let i = 0; i < arr.length; i++) {
        // filter에 전달인자로 전달된 콜백 함수는 arr의 각 요소를 전달받아 호출됩니다.
        // 콜백 함수가 true를 리턴하는 경우에만 새로운 배열에 추가됩니다.
        if (func(arr[i]) === true) {
          newArr.push(this[i]);
        }
      }
      // 콜백 함수의 결과가 true인 요소들만 저장된 배열을 리턴합니다.
      return newArr;
    };
    
    /*
     * filter 메서드의 보다 정확한 정의는 아래와 같습니다. 아래 코드를 이해하기 위해서는 다음 유닛에서 프로토타입과 this에 대한 학습이 필요합니다.
     * Array.prototype.filter = function(func) {
     *   const arr = this;
     *   const newArr = []
     *   for(let i = 0; i < arr.length; i++) {
     *     if (func(arr[i]) === true) {
     *       newArr.push(this[i])
     *     }
     *   }
     *   return newArr;
     * }
     */

filter 메서드는 배열의 요소를 콜백 함수에 다시 전달한다.

  • 콜백 함수는 전달받은 배열의 요소를 받아 함수를 실행하고, 콜백 함수 내부의 조건에 따라 참(true) 또는 거짓(false)을 리턴한다!
  • 처음 본 코드에 이 점을 반영하여 다시 코드를 작성하면, 다음과 같다
    // 함수 표현식
    const isEven = function (num) {
      return num % 2 === 0;
    };
    
    let arr = [1, 2, 3, 4];
    // let output = arr.filter(짝수);
    // '짝수'를 판별하는 함수가 조건으로서 filter 메서드의 전달인자로 전달됩니다.
    let output = arr.filter(isEven);
    console.log(output); // ->> [2, 4]
    
    const isLteFive = function (str) {
      // Lte = less then equal
      return str.length <= 5;
    };
    
    arr = ['hello', 'code', 'states', 'happy', 'hacking'];
    // output = arr.filter(길이 5 이하)
    // '길이 5 이하'를 판별하는 함수가 조건으로서 filter 메서드의 전달인자로 전달됩니다.
    let output = arr.filter(isLteFive);
    console.log(output); // ->> ['hello', 'code', 'happy']

실제 filter 활용 예시

filter 활용 시, 아래 과정을 꼭 기억하자!

  • 배열의 각 요소가
  • 특정 논리(함수)에 따르면, 사실(true)일 때
  • 따로 분류합니다(filter).
  • 함수로 작성하여 인자로 넣고 기존 배열 수정 하지 않는다

문제

만화책 식객 27권의 정보가 배열에 담겨있다. 출판 연도가 2003년인 단행본만 담은 배열을 만들어보자

수도코드

  • 배열의 각 요소 : 각 식객 1- 27권의 정보
  • 특정 논리(함수) : 책의 출판 연도가 2003년입니다. (true / false)
  • 따로 분류 : 출판 연도가 2003년인 책의 정보

실제 코드

filter는 이렇게 조건에 맞는 데이터만 분류(filtering) 할 때 사용한다

// 단행본 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
];

// 단행본 한 권의 출판 연도가 2003인지 확인하는 함수
const isCreatedAt2003 = function (cartoon) {
  const fullYear = new Date(cartoon.createdAt).getFullYear()
  return fullYear === 2003;
};

// 출판 연도가 2003년인 책의 모음
const filteredCartoons = cartoons.filter(isCreatedAt2003);

map

  • 모든 요소에게 동일한 행동을 준 값에 대하여 모두 반환한다
  • 함수로 작성하여 인자로 넣고 기존 배열 수정 하지 않는다
  • 원하는 행동에 따라서 결과가 달라진다

실제 map 활용 예시

map 활용 시, 아래 과정을 꼭 기억하기!

  • 배열의 각 요소가
  • 특정 논리(함수)에 의해
  • 다른 요소로 지정(map) 됩니다.

문제

만화책 식객 27권의 정보가 배열에 담겨있습니다. 각 책의 부제(subtitle)만 담은 배열을 만들어보자

수도 코드

  • 배열의 각 요소 : 각 식객 1- 27권의 정보
  • 특정 논리(함수) : 책 한 권의 부제를 찾습니다.
  • 다른 요소로 지정 : 각 식객 1- 27권의 부제

실제 코드

map은 이렇게 하나의 데이터를 다른 데이터로 매핑(mapping) 할 때 사용한다

// 만화책 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
];

// 만화책 한 권의 부제를 리턴하는 로직(함수)
const findSubtitle = function (cartoon) {
  return cartoon.subtitle;
};

// 각 책의 부제 모음
const subtitles = cartoons.map(findSubtitle); // ['어머니의 쌀', ...]

reduce

  • 배열을 하나의 값으로 만들어 준다
  • 초기 값을 정할 수 있는데, 정하지 않는다면 배열의 제일 첫번째 요소가 초기값이 된다
  • 초기값은 누적값의 기반이 됨 그 다음 요소부터 현재 값이 된다
  • 이 둘은 우리가 원하는 행동에 의하여 누적값에 저장이 되고 배열의 끝까지 반복을 하게 된다
  • 결국엔 누적값의 값을 반환한다
  • 만약 초기값을 정한다면 배열의 첫번째 요소가 현재값이 된다
let arr = [1,2,3]
let result = arr.reduce(
function(acc, cur, idx){
	acc+cur;
	return acc;
},1); //여기서 1은 초기값으로 1 설정한 것

result;
  • 문자열이나 숫자를 합치거나 더하거나 뺄 수도 있고, 제일 작은것 혹은 큰 것을 비교 할 수 있고, 배열 이외의 다른 형태로 만들 수 있다!
    이외에 활용할 수 있는 방법이 많이 있음

실제 reduce 활용 예시

reduce 활용 시, 아래 과정을 꼭 기억하자!

  • 배열의 각 요소를
  • 특정 방법(함수)에 따라
  • 원하는 하나의 형태로
  • 응축합니다. (reduction)

문제

만화책 식객 27권의 정보가 배열에 담겨있다. 각 단행본의 평점의 평균을 리턴하자.

수도코드

  • 배열의 각 요소 : 각 식객 1- 27권의 정보
  • 응축하는 방법 (함수) : 각 단행본의 평점을 누적값에 더합니다.
  • 원하는 형태 : 숫자로 누적합니다.
  • 응축된 결과 : 각 단행본의 평점의 합을 단행본의 길이로 나눈 평점의 평균

실제 코드

reduce는 이렇게 여러 데이터를, 하나의 데이터로 응축(reduce)할 때 사용한다.

// 단행본 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
];

// 단행본 한 권의 평점을 누적값에 더한다.
const scoreReducer = function (sum, cartoon) {
  return sum + cartoon.averageScore;
};

// 초기값에 0을 주고, 숫자의 형태로 평점을 누적한다.
let initialValue = 0
// 모든 책의 평점을 누적한 평균을 구한다.
const cartoonsAvgScore = cartoons.reduce(scoreReducer, initialValue) / cartoons.length;
  • reduce는 배열 요소의 합을 구할 때만 사용하는 것이 아니다.

reduce의 색다른 사용법

배열을 문자열로

수도 코드

  • 배열의 각 요소 : 유저 정보
  • 응축하는 방법 (함수) : 하나의 유저의 이름과 쉼표를 이어 붙입니다(concat)
  • 원하는 형태 : 문자열로 누적합니다.
  • 응축된 결과 : 쉼표로 구분되는 모든 유저의 이름
function joinName(resultStr, user) {
  resultStr = resultStr + user.name + ', ';
  return resultStr;
}

let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];

users.reduce(joinName, '');

배열을 객체로

수도 코드

  • 배열의 각 요소 : 유저 정보
  • 응축하는 방법 (함수) : 유저 한 명의 이름 중 첫 글자를 주소록 객체 속성의 키(key)로, 유저의 정보를 주소록 객체 속성의 값(value)으로 추가합니다.
  • 원하는 형태 : 주소록 객체에 누적합니다.
  • 응축된 결과 : 모든 유저의 정보가 알파벳으로 구분된 주소록
function makeAddressBook(addressBook, user) {
  let firstLetter = user.name[0];

  if(firstLetter in addressBook) {
    addressBook[firstLetter].push(user);
  } else {
    addressBook[firstLetter] = [];
    addressBook[firstLetter].push(user);
  }

  return addressBook;
}

let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];

users.reduce(makeAddressBook, {});

이제 어느정도 알아봤으니...
왜 고차함수를 쓸까??

고차함수를 쓰는 이유

추상화

  • 복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것
  • 자바스크립트를 비롯한 많은 프로그래밍 언어 역시, 추상화의 결과이다
  • 컴퓨터를 구성하는 장치(중앙처리장치, CPU; Central Processing Unit)는 0과 1만 이해하는데 크롬 개발자 도구의 콘솔(console) 탭에서 다음의 코드를 입력했을 때, 어떤 과정을 거쳐 10이 출력되는지 몰라도 10을 출력할 수 있음. 그런 복잡한 것들은 크롬의 자바스크립트 해석기(엔진)가 대신해 주기 때문이다
function sum(num1, num2) {
  return num1 + num2;
}
const output = sum(3, 7);
console.log(output); // --> 10
  • 컴퓨터의 내부 구조에 대한 고민이 추상화로 해결되었다

  • 자바스크립의 문법(syntax)을 올바르게 사용하는 것만으로, 다양한 프로그램을 (자바스크립트가 없었을 때) 보다 쉽게 작성할 수 있다.

  • 이처럼 고민거리가 줄어들고, 그래서 문제의 해결이 더 쉬워지는 것이 추상화의 이점이다

  • 추상화 = 생산성(productivity)의 향상

  • 추상화의 관점에서 함수를 바라보면, 함수는 사고(thought) 또는 논리(logic)의 묶음!

아래의 getAverage 함수는 number 타입을 요소로 갖는 배열을 입력받아, 모든 요소의 평균값을 리턴합니다. 앞으로는 number 타입을 요소로 갖는 배열을 인자로 전달하기만 하면, 복잡한 로직은 신경 쓰지 않아도 평균값을 얻을 수 있습니다.

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

let output = getAverage([1, 2, 3]);
console.log(output); // --> 2

output = getAverage([4, 2, 3, 6, 5, 4]);
console.log(output); // --> 4

그렇다면 왜 위에서 추상화를 언급했을까?

함수를 통해 얻은 추상화를, 한 단계 더 높인 것이 고차 함수이기 때문이다!

  • getAverage 함수는 값(배열)을 전달받아, 이 값을 가지고 복잡한 작업을 수행한다. 이는 값 수준에서의 추상화이다.

    함수 = 값을 전달받아 값을 리턴한다 = 값에 대한 복잡한 로직은 감추어져 있다 = 값 수준에서의 추상화

고차 함수는 이 추상화의 수준을 사고의 추상화 수준으로 끌어올린다

  • 값 수준의 추상화: 단순히 값(value)을 전달받아 처리하는 수준
  • 사고의 추상화: 함수(사고의 묶음)를 전달받아 처리하는 수준


다시 말해 고차 함수를 통해, 보다 높은 수준(higher order)에서 생각할 수 있다!

  • 고차 함수 = 함수를 전달받거나 함수를 리턴한다 = 사고(함수)에 대한 복잡한 로직은 감추어져 있다 = 사고 수준에서의 추상화

추상화의 수준이 높아지는 만큼, 생산성도 비약적으로 상승한다!

profile
개발 공부하는 코린이!

0개의 댓글