[JS] 배열 고차 함수

hangkemiii·2022년 4월 28일
0

Javascript

목록 보기
5/11
post-thumbnail

서론

너무 오랜 시간동안 map()과 forEach() 함수 등에 치이고 고통받아왔다.. repl.it 문제들이나 알고리즘, 더 나아가 리액트까지 배열 고차 함수는 정말 많은 곳에서 사용되고 중요한 부분을 차지하고 있다. 그러나 그것들에 대한 나의 지식은 정말이지 한참 부족한 수준이었다. 그러니 오늘 velog에 정리하면서 한번 제대로 짚고 넘어가보자!

배열 고차 함수

그럼 이것들의 집합인 고차 함수의 정의부터 알고 넘어가자. 고차 함수(Higher-Order Function, HOF)는 함수를 인수로 전달받거나 함수를 반환하는 함수를 말한다. 이 고차 함수를 통해 자바스크립트는 함수형 프로그래밍에 알맞은 언어로 만들어진다. 이것을 완전히 이해하기 위해서는 함수형 프로그래밍퍼스트 클래스 함수(First-Class Function)을 먼저 이해하고 넘어갈 필요가 있다.

함수형 프로그래밍(Functional Programming)?

함수형 프로그래밍(Functional Programming)이란 객체가 기본이 되는 객체지향 프로그래밍과는 달리 함수를 기반으로 하는 프로그래밍을 뜻한다. 함수형 프로그래밍에는 몇 가지 원칙이 있다.

  • 입, 출력이 순수해야 한다. (순수 함수)

  • 부작용(부산물)이 없어야 한다.

  • 함수와 데이터를 중점으로 생각한다.

입, 출력이 순수하다는 것은 반드시 하나 이상의 인자를 받고, 받은 인자를 처리하여 반드시 결과물을 돌려주어야 한다는 것을 뜻한다. 즉, 받은 인자만으로만 결과물을 내어야 하는 함수를 순수 함수라고 부른다. 자바스크립트는 this라는 개념 때문에 완벽하게 함수형 프로그래밍을 할 수 없지만, 그래도 비슷하게 구현할 수 있다.

즉, 함수를 다른 함수의 파라미터로 넘길 수도 있고, 반환(return) 값으로 받을수도 있는, 함수를 기반으로 하는 프로그래밍을 함수형 프로그래밍이라 한다.

퍼스트클래스 함수?

자바스크립트, 또는 다른 함수형 프로그래밍 언어 함수들은 전부 객체(Object)이다. 그리고 그 객체인 자바스크립트에서, 함수는 Function 객체라는 특별한 객체의 타입을 가지고 있다.

function sayHi() {
  console.log('Hi, Everyone!');
}

sayHi(); // Hi, Everyone!

sayHi.answer = 'Hello!';

console.log(sayHi.answer); // Hello!

이렇게, 함수에 프로퍼티를 추가함으로써 우리는 함수가 Object인 것을 증명할 수 있다. 그렇기 때문에 자바스크립트에서 object, string, number와 같은 타입으로 할 수 있는 것은 함수 역시 할 수 있게 된다. 함수를 파라미터로 다른 함수에 넘기거나, 함수를 다른 변수에 할당하거나, 다른 곳으로 넘길 수도 있다. 이러한 특성 때문에 자바스크립트에 존재하는 함수들은 퍼스트클래스 함수라 불린다.

고차 함수의 종류

1. Array.prototype.sort

sort 메서드는 배열의 요소를 정렬한다. 원본 배열을 직접 변경하여 정렬된 배열을 반환한다.
sort 메서드는 기본적으로 오름차순으로 요소를 정렬한다.

const fruits = ['Banana', 'Orange', 'Apple'];

// 오름차순 정렬
fruits.sort();

// sort 메서드는 원본 배열을 직접 변경한다.
console.log(fruits); // ['Apple', 'Banana', 'Orange']

한글 문자열인 요소도 오름차순으로 정렬된다.

const fruits = ['바나나', '오렌지', '사과'];

fruits.sort();
console.log(fruits); // ['바나나', '사과', '오렌지']

만일 내림차순으로 요소를 정렬하고 싶으면 reverse 메서드를 사용하여 요소의 순서를 뒤집으면 된다.

const fruits = ['Banana', 'Orange', 'Apple'];

fruits.sort();
fruits.reverse();

console.log(fruits); // ['Orange', 'Banana', 'Apple']

여태까지는 문자열 요소로 이루어진 배열의 정렬을 살펴보았다. 그러면 숫자 요소의 경우에는 어떨까? 숫자도 작은 수부터 큰 수 순서대로 정렬되겠지! 하면 큰일날 수 있다. 왜 그런지 살펴보자.

const points = [40, 100, 1, 5, 2, 25, 10];

points.sort();
console.log(points); // [1, 10, 100, 2, 25, 40, 5] => 숫자 크기순으로 정렬 X

sort 메서드의 기본 정렬 순서는 유니코드 코드 포인트의 순서를 따르기 때문에, 숫자 타입의 배열도 문자열로 일시적으로 변환시킨 다음에 유니코드 코드 포인트의 순서로 정렬시킨다. 그렇기 때문에 숫자 요소를 정렬시키려면 sort 메서드에 정렬 순서를 정의하는 비교 함수를 인수로 전달해야 한다.

비교 함수는 양수나 음수 또는 0을 반환해야 한다. 비교 함수의 반환값이 0보다 작으면 비교 함수의 첫 번째 인수를 우선하여 정렬하고, 0이면 정렬하지 않으며, 0보다 크면 두 번째 인수를 우선하여 정렬한다.

const points = [40, 100, 1, 5, 2, 25, 10];

points.sort(a, b) => a - b); // a - b가 0보다 작으면 a를 우선 정렬한다. => 오름차순 정렬
points.sort(a, b) => b - a); // a - b가 0보다 작으면 b를 우선하여 정렬한다. => 내림차순 정렬

2. Array.prototype.forEach

앞서 함수형 프로그래밍을 설명할때 함수형은 입, 출력이 순수해야 한다고 했다. (순수 함수) 그러나 조건문이나 반복문을 사용하면 로직의 흐름을 이해하기 어렵게 하고, for 문을 사용할 경우 반복을 위한 변수를 선언하고, 조건식과 증감식으로 구성되어 있어서 함수형 프로그래밍이라고 볼 수 없다.

그렇기 때문에 이 for문을 대체하기 위해 등장한 고차 함수가 바로 forEach 메서드이다. forEach 메서드는 자신의 내부에서 반복문을 통해 자신을 호출한 배열을 순회하면서 수행해야 할 처리를 콜백 함수로 전달받아 반복 호출한다.

const numbers = [1, 2, 3];
const pows = [];

// for 문으로 배열 순회
for (let i = 0; i < numbers.length; i++) {
  pows.push(numbers[i] ** 2);
}

// forEach 메서드 (numbers 배열의 모든 요소를 순회하면서 콜백 함수 반복 호출)
numbers.forEach(item => pows.push(item ** 2));

forEach 메서드는 콜백 함수를 호출할 때 3개의 인수, forEach 메서드를 호출한 배열의 요소값과 인덱스, forEach 메서드를 호출한 배열(this)을 순차적으로 전달한다.

그렇기 때문에, forEach 메서드는 원본 배열(3번째 인수, this)을 변경하지 않지만, 콜백 함수를 통해 원본 배열을 변경할 수는 있다.

const numbers = [1, 2, 3];

// 콜백 함수의 3번째 매개변수 arr는 원본 배열 numbers를 가리키기 때문에
// arr를 직접 변경하면 원본 배열이 변경된다.
numbers.forEach((item, index, arr) => { arr[index] = item ** 2;});
console.log(numbers); // [1, 4, 9]

forEach 메서드는 for 문에 비해 성능이 좋지는 않지만 가독성이 더 좋다. 따라서 요소가 대단히 많은 배열을 순회하거나 시간이 많이 걸리는 복잡한 코드, 또는 높은 성능이 필요한 경우가 아니라면 for 문 대신 forEach 메서드를 사용하는 것이 좋다

3. Array.prototype.map

내가 가장 헤맸던 map 메서드이다. map 메서드는 자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출한다. 그리고 콜백 함수의 반환값들로 구성된 새로운 배열을 반환한다. 이때 원본 배열은 변경되지 않는다.

여기서 forEach 메서드랑 같은거 아니야? 라고 할 수 있지만 forEach 메서드는 언제나 undefined를 반환하고, map 메서드는 콜백 함수의 반환값들로 구성된 새로운 배열을 반환하는 차이가 있다. 즉, forEach 메서드는 for문 대체용, map 메서드는 요소값을 다른값으로 매핑한 새로운 배열 생성용 고차함수라고 생각하면 편하다.

map 메서드가 생성하여 반환하는 새로운 배열의 length 프로퍼티 값은 map 메서드를 호출한 배열의 프로퍼티 값과 반드시 일치한다. 즉, map 메서드를 호출한 배열과 map 메서드가 생성하여 반환한 배열은 1:1 매핑한다.

map 메서드 역시 forEach 메서드와 마찬가지로 콜백 함수를 호출할 때 3개의 인수, map 메서드를 호출한 배열의 요소값과 인덱스, 그리고 map 메서드를 호출한 배열(this)을 순차적으로 전달한다.

4. Array.prototype.filter

filter 메서드는 forEach, map 메서드와 같이 자신을 호출한 모든 배열의 요소를 순회하며 인수로 전달받은 콜백 함수를 반복 호출하지만, 콜백 함수의 반환값이 true인 요소로만 구성된 새로운 배열을 반환하는 특징이 있다. map 메서드와 마찬가지로 원본 배열은 변경되지 않는다.

const numbers = [1, 2, 3, 4, 5];

// 콜백 함수의 반환값이 true인 요소로만 구성된 새로운 배열 반환
// numbers 배열에서 홀수인 요소만 필터링한다 (1은 true로 평가됨)
const odds = numbers.filter(item => item % 2);
console.log(odds); // [1, 3, 5]

filter 메서드는 자신을 호출한 배열에서 필터링 조건을 만족하는 특정 요소만 추출하여 새로운 배열을 반환하기 때문에, filter 메서드가 생성하여 반환한 새로운 배열의 length 프로퍼티 값은 filter 메서드를 호출한 배열의 length 프로퍼티 값과 작거나 같다.

filter 메서드 역시 forEach, map 메서드와 마찬가지로 콜백 함수를 호출할 때 3개의 인수, filter 메서드를 호출한 배열의 요소값과 인덱스, 그리고 filter 메서드를 호출한 배열(this)을 순차적으로 전달한다.

5. Array.prototype.reduce

reduce 메서드는 콜백 함수의 반환값을 다음 순회 시에 콜백 함수의 첫 번째 인수로 전달하면서 콜백 함수를 호출하여 하나의 결과값을 만들어 반환한다. 이때 원본 배열은 변경되지 않는다.

reduce 메서드는 앞선 메서드들과 다르게 콜백 함수에 4개의 인수, 초기값 또는 콜백 함수의 이전 반환값, reduce 메서드를 호출한 배열의 요소값과 인덱스, reduce 메서드를 호출한 배열 자체, this가 전달된다.

// 1부터 4까지 누적 구하기
const sum = [1, 2, 3, 4].reduce((accumulator, currentValue, index, array) => 
                                accumulator, currentValue, 0);

console.log(sum); // 10
구분accumulatorcurrentValueindexarray콜백 함수의 반환값
첫 번째 순회010[1,2,3,4]1
(accumulator + currentValue)
두 번째 순회121[1,2,3,4]3
(accumulator + currentValue)
세 번째 순회332[1,2,3,4]6
(accumulator + currentValue)
네 번째 순회643[1,2,3,4]10
(accumulator + currentValue)

이처럼 reduce 메서드는 초기값과 배열의 첫 번째 요소값을 콜백 함수에게 인수로 전달하면서 호출하고 다음 순회에는 콜백 함수의 반환값과 두 번째 요소값을 콜백 함수의 인수로 전달하면서 호출한다. 이러한 과정을 반복하여 reduce 메서드는 하나의 결과값을 반환한다.

reduce 메서드를 호출할 때는 언제나 초기값을 전달하는 것이 안전하다.

const sum = [].reduce((acc, cur) => acc + cur);
// TypeError: Reduce of empty array with no initial value

이처럼 빈 배열로 reduce 메서드를 호출하면 에러가 발생한다. 이때 reduce 메서드에 초기값을 전달하면 에러가 발생하지 않는다.

const sum = [].reduce((acc, cur) => acc + cur, 0); // 0 => 초기값
console.log(sum); // 0

이 밖에도 Array.prototype.some, Array.prototype.every, Array.prototype.find, Array.prototype.findIndex와 같은 다양한 종류의 고차 함수들이 더 존재한다.

결론

오늘 이렇게 고차함수의 정의와 종류에 대해 알아봤는데, 고차 함수는 결국 자바스크립트를 함수형 프로그래밍에 알맞은 언어로 만들기 위해 나타난 함수이며, 자신을 호출한 배열을 순회하며 콜백 함수를 호출한다는 공통점이 있다. map 메서드와 기타 고차 함수들에 대한 이해가 조금은 된거 같아 다행이다.

profile
Front-End Developer

0개의 댓글