이번 포스팅에선 Underscore.js 의 중요 컨셉인 ArrayLike 에 대해 알아보고자 합니다.
이게 함수형과 무슨 상관이야? 라는 생각이 들 수도 있지만 먼저 작성된 포스팅에 적었듯이 함수형의 핵심 컨셉을 Underscore.js 가 제시했기에 그 컨셉이 내부 함수에 어떻게 녹아들어있는지를 알아보려는 의도입니다.
- ArrayLike
_.each 와 _.map 등 Underscore.js 의 주요 함수는 첫 번째 인자로 아래의 4가지 타입을 받습니다.
- {}
- []
- arguments
- ArrayLike
종종 함수를 써오면서 {}, [] 즉 객체와 배열을 받는다는 건 알았지만 특히 ArrayLike 는 뭘까요.
console.info(list1[0] === 1 && list1[1] === 2 && list[2] === 3); // true
console.info(list1.length === 3); // true
list1.pop();
console.info(list1.length); // 2
for (let i = 0 ; i < list1.length ; i++) {
console.info(list1[i]);
}
// 1
// 2
위 코드만 보면 list1 의 타입은 Array 인 것 같습니다. 배열의 요소에 [0], [1] 의 형태로 접근하고 있고, length property 가 있으며 pop 메서드 호출도 Array 의 고유 메서드 호출과 동일하게 동작하죠.
하지만 답은 'list1 의 타입이 Array 인지 알 수 없다' 입니다. (물론 예시를 위해 굳이 만든 코드긴 합니다..)
console.info(list1.constructor === Array); // false
자바스크립트에는 저렇게 사용 가능한 객체로 Array 만 있는 것은 아니기 때문에, 위와 같은 결과가 나올 수 있다는 것입니다.
그럼 저기서 Array 가 아니라면 list1 은 뭘까요. arguments 일 수도 있지만 arguments 에는 pop 이 없습니다.
list1 은 아래와 같은 구조의 객체였을 수도 있습니다.
const list1 = {'0': 1, '1': 2, '2': 3, length: 3};
list1.pop = function() {
delete this[this.length - 1];
this.length--;
};
// {}
const d1 = { name: 'A' };
// []
const d2 = [1, 2, 3];
// arguments
const d3 = function() {
return arguments;
}(1, 2, 3);
// ArrayLike
const d4 = { '0': 1, '1': 2, '2': 3, length: 3 };
const d5 = 'foo';
d1 은 확실히 Array 가 아니고, d2 는 확실히 Array 이고, d3 는 arguments 객체인데 이것도 Array 는 아닙니다.
d3 나 d4 에 배열 메서드인 push 나 concat 등을 호출하면 TypeError 가 발생하죠.
즉 d2 ~ d5 는 Array 이거나 Array 같은 값인데, 이를 ArrayLike 라고 합니다. (그저 _.each 나 _.map 에게 있어선 말이죠)
Underscore.js 에서 ArrayLike 는 아래와 같이 동작합니다.
const MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
const isArrayLike = function(list) {
const length = (list === null || list === undefined) ? void 0 : list.length;
return typeof length === 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
Array 같은지를 그저 length 가 숫자인지 여부만 확인하는 것만으로 판단합니다. 그래서 앞서 예시로 보였던 d4 도 ArrayLike 로 간주되는 것입니다. 이는 아래와 같은 코드도 가능하게 합니다.
const _ = require('underscore');
const tmp = function(a, b, c){};
console.info(tmp.length); // 3
_.each(tmp, function() {
console.info(arguments);
});
// [Arguments] { '0': undefined, '1': 0, '2': [Function: tmp] }
// [Arguments] { '0': undefined, '1': 1, '2': [Function: tmp] }
// [Arguments] { '0': undefined, '1': 2, '2': [Function: tmp] }
(function(a, b, c){}).length === 3 이 true 이므로 _.each 는 이런 함수를 인자로 받아서 돌려도 에러가 나지않고 뭔가 적당한 값을 돌려줍니다. Underscore.js 의 컨셉은 마치 '개발자가 데이터를 정확히 줄것이다' 라고 하는 것 같습니다.
Underscore.js 는 데이터를 주로 다루는 라이브러리고, Lodash 도 마찬가지이며 데이터를 다루는 Sequelize, Knex.js, ORM 등의 라이브러리 안에서 핵심 역할을 하고 있습니다. 그렇지만 타입에 관대하고, 데이터형을 체크하지 않고도 데이터를 잘 다룹니다.
Underscore.js 는 꼭 필요하지 않은 경우라면 타입 체크를 거의 하지 않습니다. 그냥 실행합니다.
// Error
function func2(data) {
for (let i = 0 ; i < data.length ; i++) {} // TypeError: Cannot read property 'length' of undefined
}
func2(undefined);
// OK
function func3(data) {
for (let i = 0 ; i < getLength(data) ; i++) {}
}
func3(undefined);
function getLength(list) {
return (list === null || list === undefined) ? undefined : list.length;
}
func2 는 에러가 나지만, func3 에선 0 < undefined 가 되면서 루프를 돌지 않고 에러 없이 정상 동작하게 됩니다.
Underscore.js 에는 이런 사례가 곳곳에 아주 많습니다.
즉 이런 방법을 사용해 Underscore.js 라이브러리 자체가 어지간해서는 에러 없이 동작하도록 구현되어있습니다.
내부 이런 내용을 몰랐기에 그동안 타입 체크를 하려고 상당히 많은 if 문을 추가했던 것들이 떠오릅니다. type 체크에 급급했던 것이죠.
자바스크립트 언어 및 데이터형과 Native Helpers 에 깊은 이해가 있었다면 필요 없었을 것입니다.
물론 우리가 Native Helpers 까지 지금 깊이 이해해야 한다는게 얘기하는 포인트는 아니고, Underscore.js 가 이런 깊은 이해를 바탕으로 잘 만들어진 라이브러리 라는 것입니다.
그렇다고 Underscore.js 가 에러 날 상황을 방치하는 것은 아닙니다. 이는 Underscore.js 의 테스트 케이스를 보면 잘 알 수 있으며, 개발자가 절대 실수하지 않을 부분 정도는 에러가 발생하면 발생하는대로 둡니다.
이런 이해를 바탕으로 두고 다음 포스팅부턴 Underscore.js 의 핵심 함수들에 대해 직접 구현을 해보며 더 파고들어 보겠습니다.
'Backend > 함수형 자바스크립트' 카테고리의 다른 글
Underscore.js - _.reject & _.find (0) | 2021.08.29 |
---|---|
Underscore.js - _.filter (0) | 2021.08.21 |
Underscore.js - _.map & _.each (0) | 2021.08.14 |
Underscore.js (0) | 2021.08.01 |
함수형 자바스크립트의 시작 (0) | 2021.07.24 |