높은 수준에서 생각하기
복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것이 추상화(abstraction)이다.
우리가 살아가는 이 세상은, 추상화로 가득 차 있다. '-1'을 표현하는 현실의 방법은 존재하지 않는다. 그러나 우리는 '-1'이라는 문자를 보고, "-1은 0보다 1만큼 작은 수다."라고 설명할 수 있다. 이렇듯, 인간은 추상화를 통해 생각하고 표현한다. 추상화를 이용하면, 효율적이고 편하게 생각할 수 있기 때문이다.
브라우저 창에 주소를 입력했을 때 입력한 내용을 전파하고, 어디 서버로 갔다가 다른 서버로 가는 등 그런 복잡한 내용을, 일상생활에서는 몰라도 된다. 우리는 그저 주소창에 올바른 주소를 입력하면, 브라우저가 해당 사이트를 보여 준다는 것만 알고 있다. 스마트폰으로 카카오톡이나 페이스북 메신저를 통해 친구에게 '안녕?'이란 메시지를 보내면, 그 순간 스마트폰은 기지국과 약 20개의 메시지를 주고받지만 우린 이런 것들을 전부 알지 못하고, 알 필요도 없다. 그러나 입력창에 메시지를 입력하고 전송 버튼을 누르면, 상대방이 메시지를 받는다는 사실은 알고 있다. 자동차의 시동 버튼, 자료를 정리하는 엑셀, 지하철/버스를 타기 위한 교통 카드도 추상화의 결과이다. 일상생활에서 추상화가 아닌 것을 찾아보기 힘들 정도이다.
JavaScript를 비롯한 많은 프로그래밍 언어 역시, 추상화의 결과이다. 컴퓨터를 구성하는 장치(중앙처리장치, CPU; Central Processing Unit)는 0과 1만 이해한다. 크롬 개발자 도구의 콘솔(console) 탭에서 다음의 코드를 입력했을 때, 어떤 과정을 거쳐 10이 출력되는지 몰라도 10을 출력할 수 있다. 그런 복잡한 것들은 크롬의 JavaScript 해석기(엔진)가 대신해 주기 때문이다.
즉, JavaScript의 문법(syntax)을 올바르게 사용하는 것만으로, JavaScript가 없었을 때보다 다양한 프로그램을 보다 쉽게 작성할 수 있다. 이처럼 고민거리가 줄어들고, 그래서 문제의 해결이 더 쉬워지는 것이 추상화의 이점이다.
- 추상화 = 생산성(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)에서 생각할 수 있다.
- 고차 함수 = 함수를 전달받거나 함수를 리턴한다. = 사고(함수)에 대한 복잡한 로직은 감추어져 있다. = 사고 수준에서의 추상화
추상화의 수준이 높아지면 생산성도 비약적으로 상승할 수 있다.
HOFs: High Order Functions
사고 수준의 추상화의 예시
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 함수는 배열 메서드를 적절하게 사용하여 순차적으로 원하는 작업을 수행한다. 이 코드는 꽤 괜찮은 코드이지만, '남성'의 '평균 나이'만 구하는 작업에만 사용할 수 있다. 개선할 점을 찾아보면, '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
이처럼 고차 함수를 통해 사고 수준에서의 추상화를 달성할 수 있다. 각각의 작업은 다른 목적을 위해 재사용될 수 있다.
'FE > JavaScript' 카테고리의 다른 글
[JS] 객체 지향 프로그래밍(OOP)의 4가지 주요 개념 (0) | 2023.03.15 |
---|---|
[JS] 객체 지향 프로그래밍 - 클래스와 인스턴스 (0) | 2023.03.15 |
[JS] 내장 고차 함수 (filter, map, reduce) (0) | 2023.03.14 |
[JS] 고차함수 (0) | 2023.03.14 |
[JS] 헷갈렸던 스코프 개념 콘솔에 찍어보기 (0) | 2023.03.08 |