JavaScript 형식화 배열

JavaScript 형식화 배열 (JavaScript typed arrays)

JavaScript 형식화 배열(typed arrays)은 원시 이진 데이터에 접근하는 메커니즘을 제공하는 배열과 유사한 객체들이다. 일반 배열처럼 많은 메서드를 제공하지만, 데이터를 버퍼와 뷰 형태로 다룬다는 점이 다르다.

출처: JavaScript typed arrays - JavaScript | MDN

본문

소개 (Introduction)

JavaScript typed arrays는 원시 이진 데이터(raw binary data)에 접근하는 메커니즘을 제공하는 **배열과 유사한 객체(array-like object)**이다. 배열의 인덱싱·순회·변형과 같은 표준 배열 메서드의 대부분을 구현하기 때문에, 사용자가 복잡한 데이터 구조로도 정확히 작동할 수 있게 해준다.

WebGL 같은 기술, 그리고 파일·이미지·소켓 등 이진 데이터를 다루는 여러 표준은 typed arrays의 힘 없이는 불가능하다. typed arrays가 없었다면, 성능이 매우 저하되거나, 웹의 원시 이진 데이터를 소비하는 데 브라우저가 이를 악용하기 훨씬 쉬워지는 방식으로 데이터를 변환해야 했을 것이다.

개요 (Overview)

typed arrays는 두 가지 다른 방식으로 구성된 데이터를 함께 사용한다: **버퍼(Buffer)**와 뷰(View).

  • 버퍼는 임의의 바이트 묶음을 보유하는 객체다. 그것은 그 자체로 아무것도 "보여주지" 않는다 — 아무런 콘텍스트 없이 바이트들일 뿐이다. 유일한 동작은 그 안의 데이터를 쓰고, 복사하고, 교환하는 것이다.
  • 는 제어된 콘텍스트를 제공한다. 다시 말해, 정확히 어떤 형태의 데이터(어떤 타입, 버퍼 안의 어디에서 시작해 얼마나 많은 요소)가 버퍼 안에 있는지를 알려준다. 뷰에서 실제 데이터를 읽고 쓴다.

버퍼 (Buffer)

버퍼는 ArrayBuffer 객체로, 고정 길이의 바이트로 구성된 데이터가 거주하는 메모리 영역을 나타낸다. SharedArrayBuffer는 특정 바이트 영역을 여러 프로세스·스레드와 공유할 수 있는 버퍼 객체다.

// 16바이트 버퍼를 만든다
const buffer = new ArrayBuffer(16);

// 크기 확인 (바이트 단위)
console.log(buffer.byteLength); // 16

// 콘텐츠 확인
console.log(new Uint8Array(buffer)); // Uint8Array(16) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

뷰 (Views)

  • TypedArray 뷰 — 버퍼 내부의 다양한 숫자 타입의 배열 뷰를 제공한다. 대부분의 뷰는 각 요소가 버퍼 안의 연속된 고정 너비 값들 중 하나를 나타낸다. 예를 들어 Uint8Array는 버퍼 안의 각 바이트(8비트)를 0에서 255 사이의 정수로 취급한다. BigInt64ArrayBigUint64Array는 각 요소가 버퍼 안의 64비트 값을 64비트 정수로 취급한다. float를 위한 Float16Array, Float32Array, Float64Array도 있다.

  • DataView — 버퍼 내부의 다양한 타입으로 바이트를 읽고 쓸 수 있는 자유도가 낮은 순수 뷰이다. 임의의 타입·인덱스에 따라서, 오프셋을 조정해 데이터에 접근해야 할 때 특히 유용하다. 예를 들어 3바이트 오프셋의 Int32를 읽어야 한다면, Int32Array로는 인덱스를 4바이트의 배수로만 정렬할 수 없기 때문에 맞지 않을 때 DataView를 쓴다.

둘째 구성 요소를 정의할 수 있는 한 가지 방법은 "뷰 타입 + 뷰가 참조하는 버퍼 + 오프셋 + 요소 수"를 조합하는 것이다. 뷰는 타입화된(array-like) 데이터로 작업하게 해주며, 요소 타입의 크기와 범위에 따라 "버퍼 뷰 타입"이라고도 한다. 예:

// 같은 버퍼를 각기 다른 데이터 타입으로 바라보는 두 뷰
const buffer = new ArrayBuffer(16);

const view1 = new Uint8Array(buffer); // 16바이트를 8비트 정수 16개로
const view2 = new Uint16Array(buffer); // 16바이트를 16비트 정수 8개로

console.log(view1.length); // 16
console.log(view2.length); // 8

BufferViewTypes (버퍼 뷰 타입)

다음 표는 각 TypedArray 서브클래스의 이름·요소 크기·타입을 정리한 것이다.

타입 요소 크기(바이트) 설명 C와 동일한 타입
Int8Array 1 8비트 2의 보수 부호 있는 정수 int8_t
Uint8Array 1 8비트 부호 없는 정수 uint8_t
Uint8ClampedArray 1 8비트 부호 없는 정수(클램프됨) uint8_t
Int16Array 2 16비트 2의 보수 부호 있는 정수 int16_t
Uint16Array 2 16비트 부호 없는 정수 uint16_t
Int32Array 4 32비트 2의 보수 부호 있는 정수 int32_t
Uint32Array 4 32비트 부호 없는 정수 uint32_t
Float16Array 2 16비트 IEEE 754 부동소수점 _Float16
Float32Array 4 32비트 IEEE 754 부동소수점 float
Float64Array 8 64비트 IEEE 754 부동소수점 double
BigInt64Array 8 64비트 2의 보수 부호 있는 정수 int64_t (signed long long)
BigUint64Array 8 64비트 부호 없는 정수 uint64_t (unsigned long long)

모든 요소 크기에 대해서 아직 해당하는 뷰 타입이 없는 것도 있다. 예를 들어 128비트 정수 뷰는 없다.

같은 데이터에 대한 여러 뷰 (Multiple views on the same data)

같은 버퍼에 대해 여러 뷰를 만들 수 있다. 같은 버퍼에 서로 다른 타입의 뷰가 존재하면 각 뷰는 데이터를 자신의 타입에 맞게 해석하며, 한 뷰의 변경이 다른 뷰에도 반영된다(같은 물리적 바이트를 공유하므로).

const buffer = new ArrayBuffer(8);

const uint8 = new Uint8Array(buffer);
const int16 = new Int16Array(buffer);

uint8[0] = 0xff; // 첫 바이트에 255 기록
console.log(int16[0]); // -1 (0xffff를 부호 있는 16비트로 해석)

이처럼 같은 데이터를 여러 "형태"로 보는 것은, 예를 들어 네트워크 바이트 스트림을 다양한 구조체로 해석하거나, 이진 데이터 파싱에서 특히 유용하다.이 때문에 뷰는 크기와 정렬 제약을 존중하면서 데이터에 접근할 수 있다.

예제: 파일을 읽어 typed array로 변환 (Example: Using FileReader)

파일을 읽어 typed array로 변환하는 전형적인 예는 FileReaderFile/Blob에서 arrayBuffer() 메서드를 사용하는 것이다.

const reader = new FileReader();
reader.onload = (e) => {
  // e.target.result는 ArrayBuffer
  const arrayBuffer = reader.result;
  const bytes = new Uint8Array(arrayBuffer); // 바이트 수준 접근
  // 이제 bytes로 작업 가능...
};
reader.readAsArrayBuffer(file);

또는 Blob.prototype.arrayBuffer():

const url = URL.createObjectURL(blob);
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();

Uint8Array, Uint8ClampedArray, Uint16Array 등은 이진 데이터를 다루는 표준 파일·네트워크 API에서 자주 마주친다.

성능과 일반 배열과의 차이 (Performance)

일반 Array는 요소가 어떤 값이든(심지어 객체나 함수도) 담을 수 있고, 타입이 고정되어 있지 않다. 반면 typed array는 고정된 타입과 고정된 요소 크기를 가지므로, 엔진이 메모리 레이아웃을 최적화하고 JIT 컴파일 시 타입 정보를 활용할 수 있어 일반 배열보다 더 예측 가능하고 빠른 접근을 제공한다. 큰 이진 데이터를 다룰 때 특히 그렇다.

일반 배열과 typed array 모두 length, 인덱스 접근, forEach, map, slice 등 배열 표준 메서드의 대부분을 공유하므로, 큰 리팩터링 없이 상호 교환적으로 사용할 수 있는 경우가 많다. 단, typed array는 push/pop처럼 길이가 변하는 메서드와 일부 메서드에서 동작 차이가 있을 수 있으며, 고정 길이를 기본으로 한다.

일반적인 주의사항 (General notes)

  • 엔디언(endianness): TypedArray 뷰는 플랫폼의 기본 바이트 순서(대부분 리틀엔디언)를 사용한다. 명시적으로 특정 엔디언을 다뤄야 하는 이진 프로토콜에서는 DataView(예: getUint16(offset, littleEndian))에 엔디언 인수를 명시하는 것이 안전하다.
  • 정렬(alignment): TypedArray 뷰의 요소는 그 요소 크기의 배수로 정렬되어야 한다. 임의의 바이트 오프셋에서 특정 타입 값을 읽어야 한다면 DataView를 사용한다.
  • 범위 밖(bounds) 접근: 존재하지 않는 인덱스에 쓰면 일반 배열처럼 확장되지 않고 무시되거나, 읽으면 undefined가 아니라 타입에 따른 기본값/0이 반환될 수 있다.
  • 공유 메모리: SharedArrayBuffer를 사용하면 여러 워커가 같은 버퍼를 동시에 볼 수 있으며, Atomics를 사용해 동기화할 수 있다.

더 알아보기