6 min read

자바스크립트 배열을 객체처럼 사용할 때 생기는 일

자바스크립트 배열을 객체처럼 사용할 때 생기는 일
자바스크립트 배열을 객체처럼 사용할 때 생기는 일

Array의 type을 찍어보면 object가 출력됩니다.

const array= [1, 2, 3];
console.log(typeof array); // object

Javascript에서 Array는 사실 Dictionary(Object)입니다. v8에서는 배열 구현시 객체와 마찬가지로 Hidden Class를 사용하죠.

아래와 같은 코드를 실행하면 어떻게 될까요?

const array= [1, 2, 3];
array[6] = '7';
console.log(array); // [ 1, 2, 3, <3 empty items>, '7' ]

논리적으로는 허점이 있지만 문제없이 문자 '7'을 6번째 인덱스로 추가할 수 있습니다.

Array가 사실은 키가 숫자인 객체이기 때문에 이렇게 배열에 인덱스로 접근하여 추가하는 방식도 정상적으로 작동하게 됩니다.

에러가 나지 않는다면.. 이런 방식을 활용해도 될까요?

결론부터 말하자면 🙅‍♂️❗️ 사용을 지양해야 합니다. 비 일반적인 스타일 이기도 하고, 성능을 저하시킬 수 있기 때문에 더더욱 사용해서는 안됩니다.

1️⃣ 퍼포먼스의 저하

v8.dev/img/elements-kinds/lattice
  • v8 블로그에서 가져온 이미지입니다. v8에 따르면 이미지상 위치를 기준으로 왼쪽에서 오른쪽으로 갈수록, 그리고 위에서 아래로 갈수록 속도가 느릴 수 있다고 합니다.
    • Packed > Holey
      • 빽빽이 채워져 있는 경우보다 empty가 포함된 경우가 더 느립니다.
    • SMI(정수) > DOUBLE(실수) > 나머지
      • 타입으로 보면 정수로 구성된 경우가 가장 빠르고 그 다음으로 실수, 나머지 순입니다.
      • v8에서 내부적으로 Array를 최적화합니다. 이때 정수로 구성된 경우 SMI_ELEMENTS로 최적화하지만, 크기가 크거나 다른 고려사항이 있는 경우 다른 ELEMENTS로 최적화 될 수도 있습니다.
    • --trace-elements-transitions 플래그를 사용하면 코드 내에서 어떻게 Elements 트랜지션이 일어났는지 볼 수 있습니다.
      • 예시: node --trace-elements-transitions {filename}.js
      • 아래 코드를 플래그와 함께 돌려보면 PACKED_SMI_ELEMENTS 였던 배열이 HOLEY_ELEMENTS가 된 것을 알 수 있습니다.
const packedArray = [1,2,3,4,5];
packedArray[10] = 'test';

// ⬇ 플래그 사용한 출력결과
// elements transition [PACKED_SMI_ELEMENTS -> HOLEY_ELEMENTS] in (...생략)

이제 코드로 한번 성능 차이를 알아보겠습니다.

  • 아래는 Packed ElementsHoley Elements의 성능을 비교할 수 있는 코드입니다.
// Packed Element 배열 생성
function createPackedArray(size) {
    const array = new Array(size);
    for (let i = 0; i < size; i++) {
        array[i] = i;
    }
    return array;
}

// Holey Element 배열 생성
function createHoleyArray(size) {
    const array = new Array(size);
    for (let i = 0; i < size; i++) {
        if (i % 2 === 0) {
            array[i] = i;
        }
    }
    return array;
}

// 배열 요소에 접근하여 시간을 측정하는 함수
function measureAccessTime(array) {
    const start = process.hrtime();
    for (let i = 0; i < array.length; i++) {
        const element = array[i];
    }
    const end = process.hrtime(start);
    return end[0] * 1000 + end[1] / 1000000; // 시간(ms)으로 변환
}

// 성능 비교
const size = 999999; // 배열 크기
const packedArray = createPackedArray(size);
const holeyArray = createHoleyArray(size);

console.log("Packed Array Access Time:", measureAccessTime(packedArray), "ms"); // Packed Array Access Time: 1.65 ms

console.log("Holey Array Access Time:", measureAccessTime(holeyArray), "ms"); // Holey Array Access Time: 1.83825 ms
    • 전체가 채워진 Packed Array와 홀수 인덱스 위치는 비어있는 Holey Array를 만들어서 for문으로 각 요소에 접근합니다.
    • 전체 처리 결과 Holey Array가 약 0.2ms 더 오래걸립니다.
  • 아래는 Packed Smi ElementsPacked Elements의 성능을 비교할 수 있는 코드입니다.
 // PACKED_SMI_ELEMENTS 배열 생성
function createPackedSmiArray(size) {
    const array = new Array(size);
    for (let i = 0; i < size; i++) {
        array[i] = i; // 작은 정수 값(SMI)를 저장
    }
    return array;
}

// PACKED_ELEMENTS 배열 생성
function createPackedElementsArray(size) {
    const array = new Array(size);
    for (let i = 0; i < size; i++) {
        array[i] = { value: i }; // 객체를 저장
    }
    return array;
}

// 배열 요소에 접근하여 시간을 측정하는 함수
function measureAccessTime(array) {
    const start = process.hrtime();
    for (let i = 0; i < array.length; i++) {
        const element = array[i];
    }
    const end = process.hrtime(start);
    return end[0] * 1000 + end[1] / 1000000; // 시간(ms)으로 변환
}

// 성능 비교
const size = 1000000; // 배열 크기
const packedSmiArray = createPackedSmiArray(size);
const packedElementsArray = createPackedElementsArray(size);

console.log("PACKED_SMI_ELEMENTS Access Time:", measureAccessTime(packedSmiArray), "ms");
// PACKED_SMI_ELEMENTS Access Time: 1.363167 ms

console.log("PACKED_ELEMENTS Access Time:", measureAccessTime(packedElementsArray), "ms");
// PACKED_ELEMENTS Access Time: 1.604834 ms

    • 전체가 작은 정수로 채워진 Packed SMI Array와 객체가 저장된 Packed Array를 만들어서 for문으로 각 요소에 접근합니다.
    • 전체 처리 결과 객체가 저장된 Packed Array가 약 0.24ms 더 오래걸립니다.
  • 방금전에는 measureAccessTime에서 간단한 처리를 수행했지만, 프로세스가 복잡해지거나 배열의 크기가 커지면 커질수록 큰 성능 차이로 이어질 수 있습니다.

2️⃣ 희소 배열에 대한 메서드의 비일관적인 처리

  • Holey Array에서 empty에 대한 처리는 메서드에서 일관적으로 처리되지 않습니다. 최신 메서드에서는 undefined로 취급되고 오래된 메서드에서는 처리를 건너뜁니다.
const sparseArray = ['apple','banana','orange'];
sparseArray[10] = 'grapes';

let olderMethod = [];
sparseArray.forEach((element, index) => {
    olderMethod.push(element);
});
const newerMethodArray= Array.from(sparseArray);

console.log('Older Method', olderMethod);
console.log('Newer Method', newerMethodArray);

/* Older Method [ 'apple', 'banana', 'orange', 'grapes' ]
** Newer Method [
**  'apple',   'banana',
**  'orange',  undefined,
**  undefined, undefined,
**  undefined, undefined,
**  undefined, undefined,
**  'grapes'
/* ]

    • 이런 비일관적인 동작은 사이드 이펙트를 발생시킬 수 있고 디버깅을 어렵게 하는 요소가 될 수 있습니다.

요약

자바스크립트에서 Array는 사실 객체이지만, Key를 통해 요소를 추가하는 등 객체처럼 다루는 것은 지양해야 합니다. 그렇게 수정된 배열은 희소 배열이 될 가능성이 있으며, 희소 배열이 되면 퍼포먼스가 저하되고 메서드에서 empty 요소에 대해 비일관적으로 작동하기 때문입니다.

1️⃣ 퍼포먼스의 저하

    • v8에서 배열을 어떤식으로 다루는지 알아보았습니다.
    • 가능한 실수보다는 정수로 된 숫자 배열을 사용하고, 가능한 꽉찬 배열을 사용하도록 합시다.

2️⃣ 희소 배열에 대한 메서드의 비일관적인 처리

    • 희소배열에 대해서 메서드의 처리 방식이 일관적이지 않기 때문에 기대하지 못한 동작이 발생하거나 디버깅을 어렵게 만들 우려가 있습니다.