자바스크립트 Array는 객체다? 희소 배열이 성능과 일관성에 미치는 영향

이론
Array의 타입을 찍어보면 object가 출력됩니다.
const array = [1, 2, 3]; console.log(typeof array); // object그렇기 때문에 -논리적으로는 허점이 있지만,- 인덱스 접근으로 데이터 추가가 가능하죠.
array[6] = '7';그러나 이런 방식은 성능 저하와 메서드의 비일관적 처리를 유발할 수 있어 지양하는 것이 좋습니다.
그 이유를 지금부터 알아보도록 하겠습니다.
배경 설명
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' ]
- 이 방식은 동작하지만, 성능과 일관성 측면에서 문제가 될 수 있습니다.
권장
에러가 나지 않는다면 활용해도 될까요?
결론: 사용을 지양해야 합니다. 비일반적인 스타일이며 성능 저하를 유발할 수 있습니다.
실험
퍼포먼스의 저하
v8 블로(https://v8.dev/blog/elements-kinds)의 이미지 설명에 따르면, 왼쪽에서 오른쪽, 위에서 아래로 갈수록 속도가 느려질 수 있습니다.
Packed > Holey
SMI(정수) > DOUBLE(실수) > 나머지
v8은 내부적으로 Array를 최적화합니다.
정수로 구성된 경우 SMI_ELEMENTS로 최적화하지만,
크기나 다른 고려사항에 따라 다른 ELEMENTS로 전환될 수 있습니다.
Elements 트랜지션 확인 하는 법
// 플래그 사용 예시 (터미널에서 실행)
// node --trace-elements-transitions {filename}.js
const packedArray = [1, 2, 3, 4, 5];
packedArray[10] = 'test';
// 출력 예시 (플래그 사용 시)
// elements transition [PACKED_SMI_ELEMENTS -> HOLEY_ELEMENTS] in (...생략)
Packed vs Holey 성능 비교
// 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"); // 예: 1.65 ms
console.log("Holey Array Access Time:", measureAccessTime(holeyArray), "ms"); // 예: 1.83825 ms
- 결과: 전체가 채워진 Packed Array 대비 Holey Array는 약 0.2ms 더 오래 걸립니다. 처리 로직이 복잡하거나 배열이 커질수록 차이는 더 커질 수 있습니다.
Packed SMI vs Packed 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"); // 예: 1.363167 ms
console.log("PACKED_ELEMENTS Access Time:", measureAccessTime(packedElementsArray), "ms"); // 예: 1.604834 ms
결과: 작은 정수로 채운 Packed SMI Array가 객체가 저장된 Packed Array보다 약 0.24ms 빠릅니다.
희소 배열에 대한 메서드의 비일관적인 처리
- 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 요소 처리의 비일관성으로 인해 예측 불가능한 동작이 발생할 수 있습니다.
실무 권장 사항
가능한 정수로 구성된 숫자 배열(SMI)을 사용하기
가능한 꽉 찬(Packed) 배열을 유지하기
희소 배열을 만들지 않도록 인덱스 점프나 중간 비우기를 피하기
메서드별 empty 처리 차이를 고려해 API 선택 신중히 하기
Humonnom's Tech Blog
