이 포스트는 가장 널리 사용되고있는 라이브러리인 Lodash / Underscore.js 의 유틸리티 함수들을 순수 자바스크립트를 통해 어느 정도로 대체해 줄 수 있는지 이해를 돕기 위해 정리된 내용이다. 여기서 등장하는 몇몇 함수는 ES5에서 지원하고 몇몇은 ES6 지원이 필요하다.
Lodash 혹은 Underscore.js 같은 라이브러리의 코어 디자인은 사이드 이펙트가 없는 즉 외부 상태를 바꾸지 않는 순수 함수를 사용하는 함수형 프로그래밍
으로 되어있다. 함수형 프로그래밍의 공식적인 정의에 대해 알아보자.
함수형 프로그래밍의 컨셉에 대해 설명하는데는 여러 방법이 있지만, 간결하게 정의해보면:
함수형 프로그래밍은 계산결과를 표현의 평가로서 모델링하는 프로그래밍 스타일이다. 따라서 실행될 때 전역 상태를 변경하는 명령문으로 구성된 명령형 프로그래밍과 대조를 이룬다. 함수형 프로그래밍은 일반적으로 변경가능한 상태를 사용하지 않고 사이드 이펙트 없는 함수와 불변 데이터를 대신 사용한다.
중요한 점은 함수는 반드시 사이드 이펙트가 없어야 한다는 것이다. 그렇게 될 경우 테스트, 유지 보수, 그리고 대부분 예측가능한 것들이 쉬워진다.
그럼 이제 요점을 설명하기 위한 간단한 예제부터 복잡한 함수에 이르기까지 라이브러리 함수를 네이티브 함수로 대체하며 비교를 시작해보자.
Bit을 사용하면 라이브러리의 함수, 컴퍼넌트, 모듈을 0 리팩토링을 통해 재사용 가능할 수 있게 조각낼 수 있다.
다음은 Lodash에 대한 페이지이다.
첫번째 함수는 우리가 자주 사용하는 함수 중 하나이다. 조건을 만족하는 컬렉션에서의 첫번째 요소를 찾는 함수이다.
const users = [
{ 'user': 'joey', 'age': 32 },
{ 'user': 'ross', 'age': 41 },
{ 'user': 'chandler', 'age': 39 }
]
// Native
users.find(function (o) { return o.age < 40; })
// lodash
_.find(users, function (o) { return o.age < 40; })
매우 간단한 코드이다. 그럼 한번 두 함수의 성능을 비교해 보도록 하겠다.
다음 사이트를 통해 벤치마크 코드를 검사하고 체험 해볼 수 있다.
하지만 이 결과가 네이티브 함수가 항상 더 나은 성능을 준다고 결론 내릴 순 없다. 직접 작성하는 것이 라이브러리가 제공하는 것보다 더 낮은 성능을 제공할 수 있고, 더 복잡한 여러 기능들을 제공하지 않을 수 있다. 하지만 네이티브 함수가 좀 더 단순하고 가독성이 좋아지는 선택지라면, 확실히 더 나은 대안으로 간주해야 한다.
filter()
는 콜렉션에서 특정 조건을 만족하는 모든 요소를 추출해 준다.
const numbers = [10, 40, 230, 15, 18, 51, 1221]
_.filter(numbers, num => num % 3 === 0)
numbers.filter(num => num % 3 === 0)
콜렉션에서 첫번째 요소를 가지고 온 뒤 나머지 요소를 버리는 등 여러 방식으로 사용할 수 있다.
const names = ["first", "middle", "last", "suffix"]
const firstName = _.first(names)
const otherNames = _.rest(names)
const [firstName, ...otherNames] = names
console.log(firstName) // 'first'
console.log(otherNames) // [ 'middle', 'last', 'suffix' ]
이런 경우 spread 연산자(...
)를 사용할 수 있다. 여기서는 배열 요소를 디스트럭처링하고 있다.
first()
와 rest()
의 성능 비교는 독자들에게 예제로 남겨두도록 하겠다.
개발자들은 each()
나 forEach()
처럼 내장된 반복자 대신 for 반복문을 사용하는 것이 좋다. 그리고 이 사례는 lodash 함수를 사용하는 것이 좋은 경우 중 하나이다.
_.each([1, 2, 3], (value, index) => {
console.log(value)
})
[1, 2, 3].forEach((value, index) => {
console.log(value)
})
_.forEach({ 'a': 1, 'b': 2 }, (value, key) => {
console.log(key);
});
({ 'a': 1, 'b': 2 }).forEach((value, key) => { // !error
console.log(key);
});
벤치마킹 결과는 보는 것처럼 매우 흥미롭다. lodash의 _.each
가 명백한 승자로 나타났기 때문이다.
Lodash의 each
는 브라우저의 스펙에 따라 구현을 다르게 가져가기 때문에 훨씬 빠르다.
Lodash의 개발자들은 네이티브
forEach
의 상대적 속도가 브라우저에 따라 다르다고 설명한다.forEach
가 네이티브 함수이기 때문에 for나 while로 만들어진 단순한 루프보다 빠르다는 것을 의미하지 않는다. forEach는 첫번째로 좀 더 특별한 경우들을 처리해야 한다. 두번째로 forEach는 콜백함수를 사용하기 때문에 함수를 호출하는데 있어서 (잠재적인) 오버헤드가 존재한다.
이 함수는 배열의 모든 요소가 특정한 조건을 만족하는지 테스트한 결과를 반환한다. 그리고 이 함수는 네이티브 함수가 훨씬 빠르다!
const elements = ["cat", "dog", "bat"]
_.every(elements, el => el.length == 3)
elements.every(el => el.length == 3) //true
이 함수는 배열의 요소중 하나라도 특정한 조건을 만족하는지 테스트한 결과를 반환한다.
const elements = ["cat", "dog", "bat"]
_.some(elements, el => el.startsWith('c'))
elements.some(el => el.startsWith('c'))
콜렉션에 해당 요소를 갖고 있는지를 확인한다.
const primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,97]
_.includes(primes, 47) // true
primes.includes(79) // true
배열의 요소들중 중복값을 제거한 결과를 반환한다.
var elements = [1,2,3,1,2,4,2,3,5,3]
_.uniq(elements) // [1,2,3,4,5]
[...new Set(elements)] // [1,2,3,4,5]
위 예시에선 Set
자료 구조를 이용하여 중복을 제거한 뒤 sperad 연산자를 사용하여 다시 배열로 돌려 놓는다. 이제 2번의 전환이 실제로 도움이 되는지 살펴보자.
_.uniq
의 속도가 빨랐다.
다음 방식으로 요소를 필터링을 좀 더 효율적으로 할 수 있다.
elements.filter((value, index, array) => array.indexOf(value) === index)
네이티브 함수를 사용할 지 Lodash의 uniq를 사용할 지는 당신의 선택에 달렸다.
compact
는 배열에서 undefined혹은 falsy 값을 제거하는 유용한 함수이다.
var array = [undefined, 'cat', false, 434, '', 32.0]
_.compact(array)
array.filter(Boolean)
여기서 우린 약간의 구문적 특성을 이용해서 array.filter(Boolean)
을 통해 모든 요소를 Boolean을 통해 변환하여 truthy 값만을 반환한다.
여러 예제를 살펴보면서 우리는 외부 라이브러리보다 네이티브 함수를 선택할 때 고려해야할 면을 알 수 있었다.
이 글이 올바른 결정을 내리는데 도움이 되길 바라며, 읽어줘서 고맙다 :)!