일급 객체(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
(초기 값) 이 있다. acc
와 cur
은 콜백함수의 매개변수로 들어가며 초기값은 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