JavaScript Array Deep Dive (2) — 배열의 컴퓨터 동작 원리와 성능 최적화
자바스크립트 배열은 실제 배열이 아니다? V8 엔진과 배열 최적화의 모든 것
2화: 자바스크립트 배열의 컴퓨터 동작 원리와 성능 최적화
왜 같은 배열인데 성능이 다를까?
개발을 하다 보면 map()
하나 돌렸을 뿐인데 화면이 버벅이고, 수천 개의 데이터를 다루는 리스트에서 렉이 발생하는 경험을 하게 됩니다.
이럴 때 많은 사람들은 “React 성능 때문인가?”라고 생각하지만, 사실 자바스크립트 배열의 동작 방식을 이해하면 문제의 원인을 훨씬 더 정확히 알 수 있습니다.
이 글에서는 자바스크립트 배열이 메모리에서 어떻게 다뤄지는지, 그리고 왜 배열의 구조에 따라 성능 차이가 발생하는지를 살펴봅니다.
1. 컴퓨터에서 배열은 어떻게 저장될까?
전통적인 프로그래밍 언어(C, Java 등)의 배열은 연속된 메모리 공간에 데이터를 저장합니다.
int arr[3] = {10, 20, 30};
주소: 0x00 0x04 0x08
값: 10 20 30
- 정적 배열: 길이가 고정, 메모리 연속 할당
- 접근 속도 빠름: 인덱스를 이용해 직접 주소 계산이 가능
2. 자바스크립트 배열은 실제 배열이 아니다
자바스크립트 배열은 실제로는 객체(Object) 입니다.
각 요소는 객체의 속성처럼 키-값으로 저장되며, 이 키는 문자열로 변환된 인덱스입니다.
const arr = ['a', 'b'];
console.log(typeof arr); // 'object'
console.log(Object.keys(arr)); // ['0', '1']
- 배열은 실제로는
length
속성과 정수형 키를 가진 객체 - 배열 요소가 비연속적으로 존재할 수 있음
- 배열처럼 보여도 객체처럼 느리게 동작할 수 있음
3. V8 엔진은 배열을 최적화해서 다룬다
Google Chrome의 V8 엔진은 성능 최적화를 위해 배열을 분류합니다:
(1) Packed Elements
- 연속된 인덱스를 가진 배열
- 타입이 일정 (예: 모두 숫자)
- 빠르게 처리됨
(2) Holey Elements
- 누락된 인덱스나
undefined
,null
이 섞인 배열 - 최적화 해제 → 성능 저하
const packed = [1, 2, 3]; // Fast
const holey = [1, , 3]; // Slower
배열 중간에
delete arr[1]
또는arr[100] = 5
처럼 인덱스를 건너뛰면 V8은 해당 배열을 느린 구조로 취급합니다.
4. 배열 성능을 떨어뜨리는 흔한 실수들
1) 비연속 인덱스
const arr = [];
arr[100] = 'a'; // 0~99까지는 hole이 생김
2) 타입 혼합
const arr = [1, 2, 3];
arr.push('four'); // 배열 내부 타입이 혼합됨 → 최적화 해제
3) delete 사용
delete arr[1]; // arr[1]은 비워짐 → Hole 발생
4) sort 등에서 비교 함수 미제공
[10, 2, 30].sort(); // ['10', '2', '30']
5. 성능 최적화를 위한 배열 사용 방법
“V8 엔진의 배열 최적화는 Dense + Predictable 구조를 좋아한다”
1) unshift
, shift
보다는 push
, pop
push/pop은 배열 끝에서만 조작 → 빠르고 예측 가능 → 반대로 shift/unshift는 전체 인덱스를 밀어야 해서 느림
2) 대용량 데이터 배열 사용 시 forEach
, map
보다는 for
루프 직접 순회
forEach와 map은 선언적으로 깔끔하지만, 수천 건 이상을 순회하는 경우엔 for → forEach와 map은 내부적으로 callback function을 매번 호출 → forEach와 map은 함수를 값으로 넘기고 호출하는 과정이 반복됨
다음 코드 실행으로 속도 차이를 확인해볼 수 있습니다.
const arr = Array.from({ length: 1000000 }, (_, i) => i);
// 1. for 루프
console.time('for');
let sum1 = 0;
for (let i = 0; i < arr.length; i++) {
sum1 += arr[i];
}
console.timeEnd('for');
// 2. forEach
console.time('forEach');
let sum2 = 0;
arr.forEach(v => sum2 += v);
console.timeEnd('forEach');
// 3. map
console.time('map');
let sum3 = 0;
arr.map(v => sum3 += v);
console.timeEnd('map');
/** log 결과
for: 3.514892578125 ms
forEach: 8.708984375 ms
map: 18.7568359375 ms
*/
실무에서는 이렇게 큰 숫자로 for문을 돌릴 일이 진짜 거의 거의 거의 없기 떄문에 기억만 해두면 좋을 것 같다! 협업을 할 떄는 코드 가독성을 무시할 수 없으니까요. ㅎㅎㅎ
3) Sparse Array는 좋지 않음! 요소를 미리 채워서 초기화 하기
배열을 초기화할 땐 fill()로 모든 슬롯을 채워야 함 → 비어있으면 <3 empty items> 처럼 Sparse Array(구멍 있는 배열)로 인식됨 → 엔진 최적화 해제
4) 혼합 타입 보다는 타입을 일관되게 유지
V8 엔진은 배열이 같은 타입의 요소들로 구성되어 있으면 Packed Array로 최적화 하지만 숫자, 문자열, 객체 등 타입이 섞이면 최적화가 해제되어 느린 Generic Object 배열로 전환돼서 성능 저하가 생김 → 배열 요소는 가능한 한 동일한 타입으로 유지하는 게 좋음
5) splice
보다는 slice
slice는 원본을 유지하지만, splice는 배열을 직접 바꿈 → splice는 성능 이슈보다도 불변성 해침 + 디버깅 어려움이 문제
6. 배열은 객체다: 이것을 활용한 유연함
배열의 유연한 특성을 잘 이용하면, 굳이 객체로 안 만들고도 다양한 구조를 만들 수 있습니다.
const arr = [1, 2];
arr.foo = 'bar';
console.log(arr.foo); // 가능하지만 권장되진 않음
이처럼 배열은 유연한 반면, 퍼포먼스를 잡으려면 ‘객체처럼’ 사용하지 않는 것이 중요합니다.
마무리: 배열은 빠를 수도, 느릴 수도 있다
자바스크립트 배열은 단순 리스트로 보기엔 너무나도 복잡합니다.
그 복잡함을 이해하고 나면, 수천 건의 데이터를 다룰 때 성능 병목을 예측하고 방지할 수 있습니다.
자바스크립트 배열의 속사정을 알고 자바스크립트를 진짜 제대로 써 보자 🎖️