Inside Lodash: _.concat

 

_.concat(array, [values])

 

여러 배열들을 순서대로 하나의 배열로 이어 붙여주는 함수입니다. 이 함수의 인자로는 배열도 가능하지만 단일 값을 가진 일반 변수도 가능합니다.

이 함수를 실행하면 새로운 배열을 반환하므로 원본 배열에는 아무런 수정도 가해지지 않는다는 것이 특징입니다만,  값을 복사하는 방식이 shallow copy 이므로 오브젝트를 바라보는 변수가 복사되면 오브젝트가 새로 만들어지지 않고 기존의 오브젝트를 그대로 가리키는 새로운 변수가 만들어집니다.


var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

 

참고로 자바스크립트 언어에도 위의 명세와 같이 동일한 기능을 하는 함수가 존재합니다.

 


function concat() {
    var length = arguments.length;
    if (!length) {
        return [];
    }
    var args = Array(length - 1),
               array = arguments[0],
               index = length;

    while (index--) {
        args[index - 1] = arguments[index];
    }
    return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));
}

 

가변 인자를 받아야 하기 때문에 arguments 객체를 이용하고 있습니다. arguments 객체를 이처럼 활용하면 정해지지 않은 다수의 인자를 받는 처리가 가능한데, 따로 지정된 변수명이 없기 때문에 arguments 객체를 배열처럼 활용하게 됩니다. 하지만 실제로 이 객체는 배열이 아닙니다. 배열로 활요하고 싶다면 다음 한 줄을 추가해주어야 합니다.

 


var args = Array.prototype.slice.call(arguments);

 

함수 마지막 줄에 핵심 구현부가 포함되어 있습니다. 첫 번째 인자로 넘어온 array(혹은 단일 변수를 배열화 시킨)에 뒤이어 들어온 인자들을 push 하고 있습니다. 이 때, 뒤이어 들어온 여러 인자들을 배열의 깊이(depth)를 한 단계 감소시켜주는데 _.flatten에 사용되는 baseFlatten 함수를 이용하고 있습니다.

 


var array = [1];
var other = _.concat(array, 2, [3], [[4]]);
console.log(other);
// => [1, 2, 3, [4]]

 

arrayPush([1], [2, 3, [4]])가 되는셈이죠.

 

Lodash 4.16.2

Inside Lodash: _.compact

 

_.compact(array)

 

배열에 false로 인식 되는 모든 요소들을 깔끔하게 지운 새로운 배열을 반환해줍니다. 여기서 false로 인식되는 것들은 다음 값들입니다:

  • false
  • null
  • 0
  • “”
  • undefined
  • NaN

 


function compact(array) {
    var index = -1,
        length = array ? array.length : 0,
        resIndex = 0,
        result = [];

    while (++index < length) {
        var value = array[index];
        if (value) {
            result[resIndex++] = value;
        }
    }
    return result;
}

 

구현부를 살펴보면 false로 판정을 내리는데 자바스크립트 언어의 if절이 그대로 사용되고 있는 것을 알 수 있습니다.

재미있는 점은 resIndex라는 정수형 변수를 이용하여 직접 배열의 위치를 지정하여 값을 할당하고 있습니다. 자바스크립트에는 배열에 요소를 추가하는 push라는 간편한 함수가 제공되는데 왜 이렇게 구현하였을까요?

이 주제에 대해 많은 사람들이 논의를 한 흔적을 찾아 볼 수 있었습니다.

Is there a reason JavaScript developers don’t use Array.push()?
Why is array.push sometimes faster than array[n] = value?

결국 종합해보면 자바스크립트 언어의 array는 기존 언어들처럼 순차적인 메모리 번지를 가지는 것이 아니라 key/value 쌍으로 이루어진 map 형태이므로 direct assignment가 크게 유리하지 않는 것으로 보입니다.

실제로 위 코드의 배열에 대입하는 구문을 push로 바꾸어 테스트 해본 결과, 10번 시도의 평균값으로 direct assignment가 2.22ms, push가 1.99ms로 근소하게 push 방식이 빠르게 나타났습니다.

 

Lodash 4.16.2

 

Inside Lodash: _.chunk

 

_.chunk(array, [size=1])

 

하나의 배열을 지정한 개수만큼 여러 배열로 나누어 저장하는 함수입니다. 지정한 개수로 정확하게 나뉘지 않는다면 마지막 배열은 나뉜 나머지들로 채워지게 됩니다.

 


function chunk(array, size, guard) {
    if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) {
        size = 1;
    } else {
        size = nativeMax(toInteger(size), 0);
    }
    var length = array ? array.length : 0;
    if (!length || size < 1) {
        return [];
    }
    var index = 0,
    resIndex = 0,
    result = Array(nativeCeil(length / size));

    while (index < length) {
        result[resIndex++] = baseSlice(array, index, (index += size));
    }
    return result;
}

 

지정한 개수만큼 나누어 담기 위해 필요한 개수의 배열을 미리 선언해두고, slice를 이용해 직접 배열을 잘라서 담고 있습니다.

여러 방면에서 유용하게 사용 될 수 있겠지만, 생각해 볼 수 있는 예제 중 하나로는 처리해야 하는 큰 데이터 셋을 여러 작업자가 나누어 담당 할 때 작업량을 균등하게 나누어 주는 용도로 사용할 수 있겠습니다.

 


var async = require('async');
var fs = require('fs');

const array = new Array(1000).fill('./sample.txt');

function run(array, callback) {
    async.eachSeries(array, (filename, _callback) => {
        fs.createReadStream(filename)
            .on('data', function() {})
            .on('end', function() {
                _callback(null);
            });
    }, callback)
}
console.time('using _.chunk');
const slicedArrays = _.chunk(array, 100);
async.each(
    slicedArrays,
    (slicedArray, callback) => run(slicedArray, callback),
    (err) => console.timeEnd('using _.chunk')
);

console.time('not using _.chunk');
run(array, (err) => console.timeEnd('not using _.chunk'));

 

파일을 1,000번 읽는 작업을 두 가지 방법으로 시도하였습니다. 첫 번째는 _.chunk를 이용하여 100개씩 10개의 배열로 나누고 async.each 함수를 이용해 병렬로 처리하는 방법입니다. 두 번째는 간단하게 전체 배열 아이템을 순차적으로 읽는 방법입니다.

시도 할 때 마다 조금씩 달라지긴 하지만 대충 결과는 다음과 같습니다:

  • using _.chunk: 288.824ms
  • not using _.chunk: 556.763ms

 

_.chunk를 이용하여 I/O 작업을 병렬로 처리하는 것이 수행 시간을 약 두 배 정도 줄여주었습니다. OS를 괜히 놀리기 보다는 감당 할 수 있는 수준으로 일의 양을 조절하며 시키는 것이 성능 향상을 위해 좋겠죠.

 

Lodash 4.16.1