자바스크립트 배열을 객체처럼 사용할 때 생기는 일
자바스크립트 배열을 객체처럼 사용할 때 생기는 일
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 블로그에서 가져온 이미지입니다. 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가 된 것을 알 수 있습니다.
- 예시: node
const packedArray = [1,2,3,4,5];
packedArray[10] = 'test';
// ⬇ 플래그 사용한 출력결과
// elements transition [PACKED_SMI_ELEMENTS -> HOLEY_ELEMENTS] in (...생략)
이제 코드로 한번 성능 차이를 알아보겠습니다.
- 아래는
Packed Elements
와Holey 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 Elements
와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");
// 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️⃣ 희소 배열에 대한 메서드의 비일관적인 처리
- 희소배열에 대해서 메서드의 처리 방식이 일관적이지 않기 때문에 기대하지 못한 동작이 발생하거나 디버깅을 어렵게 만들 우려가 있습니다.