Front End/JavaScript

고차함수

일급 객체(first-class citizen)

비행기에 퍼스트 클래스, 이코노미 클래스가 나뉘어 다른 대우를 받는 것 처럼 JavaScript에는 특별한 대우를 받는 일급 객체가 존재한다. 대표적인 일급 객체 중 하나가 함수다. 함수는 아래와 같은 특별 취급을 받는다.

  • 변수에 할당(assignment) 할 수 있다
  • 다른 함수의 전달인자(argument)로 전달 될 수 있다
  • 다른 함수의 결과로서 리턴될 수 있다

함수를 변수에 할당할 수 있기 때문에 함수를 배열의 요소, 객체의 속성값으로 저장할 수 있고 함수를 데이터(string, number, boolean, array, object)처럼 다룰 수 있다.

// 변수 square에 함수를 할당하는 함수 표현식

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

// 변수 square에 함수가 할당되어 있으므로 함수 호출 연산자 '()' 사용 가능
console.log(square(7));

고차 함수(higher order function)

고차함수는 함수를 전달인자(argument)로 받을 수 있고, 함수를 리턴할 수 있는 함수다. 앞서 다뤘듯 함수는 변수에 저장할 수 있고 함수를 담은 변수를 전달인자로 받을 수 있다. 또한 함수 내부에서 변수에 함수를 할당할 수도 있고, 함수는 이 함수를 담은 변수를 리턴할 수 있다. (이 상황에서 변수에 할당하지 않고 함수를 바로 이용할 수도 있다)

이때 다른 함수(caller)의 전달인자(argument)로 전달되는 함수를 콜백 함수(callback function)이라 한다. 어떤 작업이 완료되었을 때 호출되는 경우가 많아, 답신 전화를 뜻하는 이름이 붙여졌다.콜백 함수를 전달받은 고차 함수(caller)는 함수 내부에서 이 콜백 함수를 호출(invoke)할 수 있고, 조건에 따라 콜백 함수의 실행 여부도 결정 가능하다.

함수를 리턴하는 함수는 모양새가 특이한 만큼 커링 함수라고 따로 이름을 붙여 부른다. 커링 함수는 고차 함수의 한 종류라 할 수 있지만, 커링 함수라는 용어를 사용할 경우 고차 함수를 ‘함수를 전달인자로 받는 함수’에 한정하여 사용하기도 한다.

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

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

// 다른 함수를 인자로 받는 고차 함수
// 이 때, func에 함수가 들어온다면 func는 doubleNum의 콜백 함수
function doubleNum(func, num) {
	return func(num);
}

let output = doubleNum(double, 4);
console.log(output); // 8

함수를 리턴하는 경우

// 다른 함수를 리턴하는 고차 함수
// 함수 adder는 인자 한 개를 입력받아서 익명 함수를 리턴
// 리턴되는 익명 함수는 인자 한개를 받아 added와 더한 값을 리턴함
function adder(added) {
	return function (num) {
		return num + added;
	};
};

// adder(5)는 함수이므로 함수 호출 연산자 '()' 사용 가능
let output = adder(5)(3); // 8
console.log(output); // 8

// adder가 리턴하는 함수를 변수에 할당 가능 (일급 객체이기 때문에)
const add3 = adder(3);
output = add3(2);
console.log(output); // 5

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

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

// 함수 doubleAdder는 고차 함수
// 이 때, func에 함수가 들어온다면 func는 doubleAdder의 콜백 함수
function doubleAdder(added, func) {
	const doubled = func(added);
	return function (num) {
		return num + doubled;
	};
};

// 함수 doubleAdder에 전달인자로 함수 double을 전달
// 함수 내부에서 콜백되어 전달인자를 *2 함
// 이후 doubleAdder에서 리턴하는 익명함수의 인자로 3을 전달해 3을 더함
doubleAdder(5, double)(3); // 13

// doubleAdder가 리턴하는 함수를 변수에 할당 가능 (일급 객체이기 때문에)
const addTwice3 = doubleAdder(3, double);
addTwice3(2); // 8

내장 고차 함수

JavaScript에는 기본적으로 내장된 고차 함수가 있다. 그 중 배열 메서드 일부가 대표적인 고차 함수에 해당된다.

Array.filter()

모든 배열의 요소 중 특정 조건을 만족하는(true) 요소를 걸러내 새로운 배열을 리턴한다. 이 때, 전달되는 인자는 함수로서 특정 조건에 해당되는지 아닌지를 판별하여 불리언 값을 리턴하는 함수가 쓰인다. 즉, 함수의 리턴 값이 true인 값을 남긴다.

// ex. 배열 내의 숫자 중 짝수만 걸러낸다
let arr = [1, 2, 3, 4];
let output = arr.filter(짝수); // 짝수에는 짝수를 판별하는 함수가 들어가야 한다
console.log(output); // [2, 4]

// Arr.filter의 동작 방식
let arr = [1, 2, 3];
// 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 사용 예
// 짝수인지 판별하여 불리언 값을 리턴하는 함수
const isEven = function (num) {
  return num % 2 === 0;
};

let arr = [1, 2, 3, 4];
// '짝수'를 판별하는 함수가 조건으로서 filter 메서드의 전달인자로 전달된다.
let output = arr.filter(isEven);
console.log(output); // [2, 4]

Array.map()

모든 배열의 요소에 접근하여 전달인자인 함수를 사용해 함수의 결과값을 기존 배열에 다시 지정해준다. (새로운 배열을 리턴하지 않음) 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); // ['어머니의 쌀', ...]

Array.reduce()

배열을 하나의 값으로 응축(reduction)한다. 이 때, 응축하는 방법은 전달인자인 함수가 결정하며 결과값으로 리턴되는 데이터 값을 콤마,로 구분해 미리 정해줄 수 있다. 미리 정하지 않으면 배열의 첫번째 값이 데이터 값이 된다.

reduce의 콜백 함수는 꼭 필요한 핵심 인자들로 acc(기존값, 응축값), cur(현재 값), init(초기 값) 이 있다. acccur은 콜백함수의 매개변수로 들어가며 초기값은 reduce의 전달 인자로 입력이 가능하다.

reduce는 앞선 예처럼 전달인자인 함수에 따라 문자열이나 숫자를 합치거나 뺄 수 있고, 가장 큰 값이나 작은 값을 비교할 수 있으며, 데이터 값을 미리 정하는 것에 따라 배열 이외의 다른 형태(boolean, object 등)로도 만들 수 있다.

// 단행본 모음
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;

ex. 배열을 객체로 만드는 예제

// reduce에서 사용할 콜백함수
function makeAddressBook(addressBook, user) {
	// firstLetter는 users 내부 객체의 name 값 중 첫번째 문자열
  let firstLetter = user.name[0];

	// 만약 addressBook에 firstLetter가 이미 존재한다면
  if(firstLetter in addressBook) {
		// firstLetter 키에 객체값을 push
    addressBook[firstLetter].push(user);
	// addressBook에 firstLetter가 없다면
  } else {
		// firstLetter라는 새로운 키를 생성하고 값을 배열로 할당한 후 객체값을 push
    addressBook[firstLetter] = [];
    addressBook[firstLetter].push(user);
  }
	// 배열을 키 값으로 가진 객체 addressBook을 리턴
  return addressBook;
}

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

users.reduce(makeAddressBook, {});

고차 함수의 중요성 : 추상화

추상화

복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것을 뜻하며 이는 실생활은 물론 컴퓨터 공학의 근간을 이루는 개념 중 하나이다.

컴퓨터를 구성하는 장치는 단순히 0과 1만 이해하지만 우리는 크롬 개발자 도구의 콘솔 탭에서 다음의 코드를 입력했을 때 어떤 과정을 거쳐 10이 출력되는지 몰라도 10을 출력할 수 있다. 복잡한 것은 크롬의 엔진이 대신해주기 때문이다. 이 또한 추상화라 볼 수 있으며 예를 통해 추상화는 생산성(productivity)의 향상을 가져다 주는 것을 알 수 있다.

function sum(num1, num2) {
  return num1 + num2;
}
const output = sum(3, 7);
console.log(output); // --> 10

프로그램을 작성할 때 우리는 자주 반복해서 사용하는 로직을 별도의 함수로 작성한다. 이 역시 추상화의 좋은 사례이며 추상화의 관점에서 함수는 사고(thought) 또는 논리(logic)의 묶음이다.

값 수준에서의 추상화

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

위의 getAverage 함수는 number 타입을 요소로 갖는 배열을 받아 모든 요소의 평균값을 리턴한다. number타입의 배열을 전달인자로 사용해 함수 getAverage를 호출하기만 하면 복잡한 로직을 신경쓰지 않아도 평균값을 얻을 수 있다. 하지만 위의 예제는 단순히 값을 전달받아 리턴하기 때문에 값 수준에서의 추상화이다.

사고 수준에서의 추상화

함수를 통해 얻은 추상화를 한 단계 더 높인 것이 고차 함수이다. 고차 함수는 함수(사고)를 전달받아 처리하기 때문에 사고 수준에서의 추상화라 할 수 있다. 다시 말해 고차 함수를 통해 보다 높은 수준에서 생각이 가능하다.

사고 수준의 추상화의 예시

const data = [
  {
    gender: 'male',
    age: 24,
  },
  {
    gender: 'male',
    age: 25,
  },
  {
    gender: 'female',
    age: 27,
  },
  {
    gender: 'female',
    age: 22,
  },
  {
    gender: 'male',
    age: 29,
  },
];

위와 같이 주어진 데이터를 순차적으로 처리하려 할 때, 모든 작업을 하나의 함수로 작성할 수 있다. 예를 들어, 남성들의 평균 나이를 구한다면 아래와 같은 함수를 작성할 수 있다.

function getAverageAgeOfMaleAtOnce(data) {
  const onlyMales = data.filter(function (d) {
    // data.filter를 통해 배열의 요소 중 남성인 요소만 남긴다.
    return d.gender === 'male';
  });

  const numOfMales = onlyMales.length;

  const onlyMaleAges = onlyMales.map(function (d) {
    // onlyMales.map으로 배열의 요소에 나이라는 값을 찾아 넣은 새로운 배열을 리턴한다.
    return d.age;
  });

  const sumOfAges = onlyMaleAges.reduce(function (acc, cur) {
    // onlyMaleAges.reduce로 배열 내의 모든 값을 더한다.
    return acc + cur;
  }, 0);

  return sumOfAges / numOfMales;
}

위에 작성된 getAverageAgeOfMaleAtOnce 함수는 배열 메서드를 사용하여 순차적으로 작업을 수행하지만, ‘남성’의 ‘평균 나이’만 구하는 작업에서만 사용할 수 있다. 매개 변수를 추가해 여성의 평균 나이도 구할 수 있지만 여전히 ‘평균 나이를 구하는 작업’만 수행한다.

추상화는 고차 함수를 통해 보다 쉽게 달성할 수 있다. 각각의 작업을 별도의 함수로 분리하여 각 기능을 담당하는 함수를 결합하는 고차 함수를 작성한다면, 각각의 작업은 다른 목적을 위해서 재사용 될 수 있으므로 사고 수준에서의 추상화를 달성할 수 있다.

// filter를 이용해 남성에 해당하는 요소만 남기는 함수
function getOnlyMales(data) {
  return data.filter(function (d) {
    return d.gender === 'male';
  });
}

// map을 이용해 배열의 요소 내 나이를 찾아 새로운 배열로 만들어주는 함수
function getOnlyAges(data) {
  return data.map(function (d) {
    return d.age;
  });
}

// reduce를 이용해 배열에 존재하는 숫자를 모두 더하고 평균을 내는 함수
function getAverage(data) {
  const sum = data.reduce(function (acc, cur) {
    return acc + cur;
  }, 0);
  return sum / data.length;
}

function compose(...funcArgs) {
  // 여러 개의 함수를 인자로 전달받아 함수를 리턴하는 고차 함수
  // compose가 리턴하는 익명 함수는 임의의 타입의 data를 입력받아,
  return function (data) {
    // funcArgs의 요소인 함수들을 차례대로 적용시킨 결과를 리턴
    let result = data;
    for (let i = 0; i < funcArgs.length; i++) {
			// result에 함수를 실행한 후 그 리턴 값을 재할당 및 반복
      result = funcArgs[i](result);
    }
    return result;
  };
}

// compose를 통해 함수들이 순서대로 적용된다는 것을 직관적으로 드러내고
// 각각의 함수는 다른 목적을 위해 재사용(reuse) 될 수 있다.
const getAverageAgeOfMale = compose(
  getOnlyMales, // 배열을 입력받아 배열을 리턴하는 함수
  getOnlyAges, // 배열을 입력받아 배열을 리턴하는 함수
  getAverage // 배열을 입력받아 `number` 타입을 리턴하는 함수
);

const result = getAverageAgeOfMale(data);
console.log(result); // --> 26


Uploaded by N2T