2023. 1. 12. 12:45ㆍ코드스테이츠/코드스테이츠S2: Chapter & Unit
일급객체
비행기에는 퍼스트 클래스(first-class)가 있습니다. 이코노미 클래스와는 탑승수속부터 기내식, 수하물이 나오는 순서까지 항공사와 관련된 모든 부분에서 혜택이 다릅니다. 퍼스트 클래스 좌석을 구매한 사람은 비싼 가격을 치르고, 항공사로부터 특별한 대우를 받습니다.
JavaScript에도 특별한 대우를 받는 일급 객체(first-class citizen)가 있습니다. 대표적인 일급 객체 중 하나가 함수입니다. JavaScript에서 함수는 아래와 같이 특별하게 취급됩니다.
- 변수에 할당(assignment) 할 수 있다. // let result = functionA();
- 다른 함수의 전달인자(argument)로 전달될 수 있다. // functionA(functionB);
- 다른 함수의 결과로서 리턴될 수 있다. // return functionA
함수를 변수에 할당할 수 있기 때문에, 함수를 배열의 요소나 객체의 속성값으로 저장할 수 있습니다. 함수를 데이터(string, number, boolean, array, object)처럼 다룰 수 있습니다.
- 변수에 함수를 할당하는 경우
/*
* 아래는 변수 square에 함수를 할당하는 함수 표현식입니다.
* JavaScript에서 함수는 일급 객체이기 때문에 변수에 할당할 수 있습니다.
*
* 함수 표현식은 할당 전에 사용할 수 없습니다.
* square(7); // --> ReferenceError: Can't find variable: square
*/
const square = function (num) {
return num * num;
};
// 변수 square에는 함수가 할당되어 있으므로 (일급 객체), 함수 호출 연산자 '()'를 사용할 수 있습니다.
output = square(7); // = function (7) = 매개변수 * 매개변수라는 함수
console.log(output); // --> 49
고차 함수의 이해
고차 함수(higher order function)는 함수를 전달인자(argument)로 받을 수 있고, 함수를 리턴할 수 있는 함수입니다. 이전 콘텐츠에서 확인했듯이, 함수는 변수에 저장할 수 있습니다. 그리고 함수는, 함수를 담은 변수를 전달인자로 받을 수 있습니다. 마찬가지로, 함수 내부에서 변수에 함수를 할당할 수 있습니다. 그리고 함수는 이 변수를 리턴할 수 있습니다. 여기서 변수에 할당하지 않고 함수를 바로 이용할 수 있습니다. 어떤 고차 함수에 함수를 전달인자로 전달하고, 고차 함수는 함수 자체를 리턴합니다. 변수가 빠졌을 뿐, 동일하게 동작합니다.
- 함수는 변수에 저장할 수 있습니다.
- 함수는, 함수를 담은 변수를 전달인자로 받을 수 있습니다.
- 함수 내부에서 변수에 함수를 할당할 수 있습니다.
- 그리고 함수는 이 변수를 리턴할 수 있습니다.
- 여기서 변수에 할당하지 않고 함수를 바로 이용할 수 있습니다.
이때 다른 함수(caller)의 전달인자(argument)로 전달되는 함수를 콜백 함수(callback function)라고 합니다. 어떤 작업이 완료되었을 때 호출하는 경우가 많아서, 답신 전화를 뜻하는 콜백 함수라는 이름이 붙여졌습니다.
- 함수(caller)의 전달인자(argument)로 전달되는 함수를 콜백 함수(callback function)라고 합니다.
콜백 함수를 전달받은 고차 함수(caller)는, 함수 내부에서 이 콜백 함수를 호출(invoke)할 수 있고, 조건에 따라 콜백 함수의 실행 여부를 결정할 수도 있습니다. 아예 호출하지 않을 수도 있고, 여러 번 실행할 수도 있습니다. 특정 작업의 완료 후에 호출하는 경우는 이후에 충분히 접할 수 있습니다.
'함수를 리턴하는 함수'는 모양새가 특이한 만큼, 부르는 용어가 따로 있습니다. '함수를 리턴하는 함수'를 고안해 낸 논리학자 하스켈 커리(Haskell Curry)의 이름을 따, 커링 함수라고 합니다. 따로 커링 함수라는 용어를 사용하는 경우에는, 고차 함수라는 용어를 '함수를 전달인자로 받는 함수'에만 한정해 사용하기도 합니다. 그러나 정확하게 구분하자면, 고차 함수가 커링 함수를 포함합니다. 이번 유닛부터는 '함수를 리턴하는 함수'와 '함수를 전달인자로 받는 함수' 모두, 고차 함수로 사용합니다.
1.다른 함수를 인자로 받는경우
function double(num){ //함수double은 매개변수에 2를 곱해주는 함수.
return num * 2;
}
function doubleNum(func, num){// 함수 doubleNum은 콜백함수와 num을 입력받아 콜백함수의 매개변수로 num을 전달해주는 함수
return func(num);
}
/*
* 함수 doubleNum은 다른 함수를 인자로 받는 고차 함수입니다.
* 함수 doubleNum의 첫 번째 인자 func에 함수가 들어올 경우
* 함수 func는 함수 doubleNum의 콜백 함수입니다.
* 아래와 같은 경우, 함수 double은 함수 doubleNum의 콜백 함수입니다.
*/
let output = doubleNum(double, 4);
console.log(output); // -> 8
2.함수를 리턴하는 경우
function adder(8) { //다른 함수를 리턴하는 고차 함수.
return function (12) { // 매개변수를 받아서 고차함수의 매개변수와 더해주는 함수.
return num + added; // 20
};
}
/*
* 함수 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
3.함수를 인자로 받고, 함수를 리턴하는경우
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
내장고차함수 이해하기
JavaScript에는 기본적으로 내장된 고차 함수가 여럿 있습니다. 그중에서 배열 메서드들 중 일부가 대표적인 고차 함수에 해당합니다. 이번 콘텐츠에서는, 배열 메서드 중 하나인 filter를 학습합니다.
배열의 filter 메서드는, 모든 배열의 요소 중에서 특정 조건을 만족하는 요소를 걸러내는 메서드입니다.
예를 들어 number 타입을 요소로 갖는 배열에서 짝수만을 걸러내거나, 18 보다 작은 수만을 걸러냅니다.
string 타입을 요소로 갖는 배열에서, 길이가 10 이하인 문자열만 걸러내거나, 'korea' 같은 특정 문자열만 걸러낼 수도 있습니다
- filter 메서드는, 모든 배열의 요소 중에서 특정 조건을 만족하는 요소를 걸러내는 메서드입니다.
- 배열의 각 요소가
- 특정 논리(함수)에 따르면, 사실(true)일 때
- 따로 분류합니다(filter).
// 아래 코드에서 '짝수'와 '길이 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) 됩니다.
문제
만화책 식객 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); // ['어머니의 쌀', ...]
map과 filter의 차이
실제 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의 색다른 사용법
배열을 문자열로
수도 코드
- 배열의 각 요소 : 유저 정보
- 응축하는 방법 (함수) : 하나의 유저의 이름과 쉼표를 이어 붙입니다(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, {});
고차함수와 추상화
사고 수준의 추상화의 예시
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는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 그 결과가 true인 요소만을 갖는 배열을 리턴합니다.
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는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 각 결과를 두 번째 인자로 전달받은 초기값(0)에 누적한 결과를 리턴합니다.
return acc + cur;
}, 0);
return sumOfAges / numOfMales;
}
[코드] 남성들의 평균 나이를 구하는 하나의 함수 getAverageAgeOfMaleAtOnce위에 제시된 getAverageAgeOfMaleAtOnce 함수는 배열 메서드를 적절하게 사용하여 순차적으로 원하는 작업을 수행합니다. 이 코드는 꽤 괜찮은 코드이지만, '남성'의 '평균 나이'만 구하는 작업에만 사용할 수 있습니다. 개선할 점을 찾아보면, 'male'을 매개변수화(parameterization) 하여 조금 더 일반적인(generic) 함수로 변경할 수 있습니다. 이렇게 수정하더라도, 어디까지나 '남성' 또는 '여성'의 '평균 나이'를 구하는 작업만 수행할 수 있습니다.
한편, filter, map, reduce 등의 배열 메서드는 다른 목적을 위해서 사용될 수도 있습니다. 예를 들어 '남성' 중 '최연소 나이'를 구하거나, '여성' 중 '최연소 나이와 최연장 나이의 차이'를 구할 때, 이미 작성된 로직을 그대로 쓸 수 있습니다.
추상화는 고차 함수를 통해, 보다 쉽게 달성할 수 있습니다. 아래의 compose 함수는 입력받은 함수를 순서대로 결합하는 고차 함수입니다. 각각의 작업(filter, map, reduce)은 별도의 함수로 분리되어, compose의 인자로 전달되는 콜백 함수가 됩니다.
function getOnlyMales(data) {
return data.filter(function (d) {
return d.gender === 'male';
});
}
function getOnlyAges(data) {
return data.map(function (d) {
return d.age;
});
}
function getAverage(data) {
const sum = data.reduce(function (acc, cur) {
return acc + cur;
}, 0);
return sum / data.length;
}
function compose(...funcArgs) {
// compose는 여러 개의 함수를 인자로 전달받아 함수를 리턴하는 고차 함수입니다.
// compose가 리턴하는 함수(익명 함수)는 임의의 타입의 data를 입력받아,
return function (data) {
// funcArgs의 요소인 함수들을 차례대로 적용(apply)시킨 결과를 리턴합니다.
let result = data;
for (let i = 0; i < funcArgs.length; i++) {
result = funcArgs[i](result);
}
return result;
};
}
// compose를 통해 함수들이 순서대로 적용된다는 것이 직관적으로 드러납니다.
// 각각의 함수는 다른 목적을 위해 재사용(reuse) 될 수 있습니다.
const getAverageAgeOfMale = compose(
getOnlyMales, // 배열을 입력받아 배열을 리턴하는 함수
getOnlyAges, // 배열을 입력받아 배열을 리턴하는 함수
getAverage // 배열을 입력받아 `number` 타입을 리턴하는 함수
);
const result = getAverageAgeOfMale(data);
console.log(result); // --> 26
[코드] 입력된 함수를 순차적으로 실행하는 고차 함수 compose'코드스테이츠 > 코드스테이츠S2: Chapter & Unit' 카테고리의 다른 글
Unit.5 [React]Router (1) | 2023.01.24 |
---|---|
Unit.5 [React]SPA의 개념 (0) | 2023.01.24 |
자바스크립트 비동기[Promise] (0) | 2023.01.18 |
Unit.2 자바스크립트 프로토타입 (0) | 2023.01.16 |
Unit.9 객체지향 프로그래밍 주요 개념. (0) | 2023.01.13 |