책장/Javascript

ArrayBuffer, binary arrays

TERAJOO 2024. 1. 5. 19:50

해당 포스팅은 https://ko.javascript.info/arraybuffer-binary-arrays 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다.


1. ArrayBuffer, binary arrays

웹 개발을 진행하며 파일을 다루거나 이미지를 다루는 경우가 있는데, 이 때 이진 데이터를 많이 접할 수 있다.

이 때 ArrayBuffer, Unit8Array, DataView, Blob, File 등 여러가지 클래스를 보게되어 혼란스러울 수 있는데, 막상 정리해보면 간단해진다.

기본 이진 객체는 ArrayBuffer라고 한다. 고정된 길이의 연속적인 메모리 영역에 대한 참조 인데 다음 코드가 해당 객체를 사용하는 간단한 예시이다.

let buffer = new ArrayBuffer(16);
alert(buffer.byteLength) // 16

위의 예시는 16바이트의 연속된 메모리 영역을 할당하고 이를 모두 0으로 채우는 코드이다.

여기서 주의할 점은 ArrayBuffer 객체 네이밍에 Array 가 들어갔다고 해서 Array 객체의 공통점을 가지고 있지는 않다는 점이다. 차이점은 크게 3가지를 들 수 있다.

  1. ArrayBuffer 는 Array와 다르게 고정된 길이를 가지고 있다. (increase, decrease X)
  2. ArrayBuffer는 메모리에서 처음 정의된 만큼만 공간을 차지한다.
  3. ArrayBuffer의 개별 바이트 값에 접근하기 위해서는 따로 view 객체가 필요하다. Array 처럼 바로 인덱스를 통해 접근할 수 있는 형태가 아니다.

정리하자면 ArrayBuffer 는 메모리 공간으로써 그 안에 어떤 값이 있는지 타입을 따로 가지고 있거나 하지 않다. 그냥 연속된 바이트 공간만을 나타낸다.

1.1. View Object

이러한 연속된 바이트 공간을 다루기 위해서는 “view” 객체가 필요하다.

뷰 객체는 자체적으로 어떤 것도 저장하지 않고 있다. 그냥 ArrayBuffer 에 저장된 바이트를 해석하고 해석된 형식으로 제공하는 역할만 한다. 아래가 대표적인 뷰 객체들이다.

  • Unit8Array : 각 ArrayBuffer 안의 바이트를 별도의 숫자로 취급, 0부터 255(2^8)-1까지의 값일 수 있다. 8-bit unsigned integer 의 약자라고도 볼 수 있다.
  • Uint16Array : 각 2바이트를 하나의 정수로 취급, 0부터 65535(2^16-1) 까지의 값일 수 있다. 16-bit unsigned integer 의 약자라고 볼 수 있다.
  • Uint32Array : 각 4바이트를 하나의 정수로 취급, 0부터 4294967295(2^64-1) 까지의 값일 수 있다. 32-bit unsigned integer 의 약자라고 볼 수 있다.
  • Float64Array : 각 8바이트를 하나의 정수로 취급, 5.0x10-324 to 1.8x10308 값일 수 있다.

ArrayBuffer 는 기본 객체로써 해당 객체를 읽거나 다루기 위해서는 위의 view 객체들을 사용해야 한다.

let buffer = new ArrayBuffer(16); // create a buffer of length 16

let view = new Uint32Array(buffer); // treat buffer as a sequence of 32-bit integers

alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer

alert(view.length); // 4, it stores that many integers
alert(view.byteLength); // 16, the size in bytes

// let's write a value
view[0] = 123456;

// iterate over values
for(let num of view) {
  alert(num); // 123456, then 0, 0, 0 (4 values total)
}

 

2. TypedArray

Uint8Array, Uint32Array의 공통점은 TypeArray 이라는 점이다. 이러한 TypeArray는 같은 메소드 및 속성을 가지고 있다.

TypeArray 가 생성자를 가지고 있는 클래스인건 아니고 단지 ArrayBuffer 를 기반으로 하는 View 객체들을 가리키는 일반적인 용어이다.

"new TypedArray"를 보면 Int8Array, Uint8Array 등의 어떤 것이든 상관없이 해당 TypedArray 생성자의 인스턴스를 의미이다.

TypedArray는 일반 배열처럼 동작하며 인덱스가 있고 반복 가능하긴 하지만,
TypedArray 생성자 (Int8Array 또는 Float64Array와 같은)는 인수의 유형에 따라 다르게 동작한다.

아래의 코드가 다섯가지 인수의 형태이다.

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
  1. new TypedArray(buffer, [byteOffset], [length]);
    • ArrayBuffer 인스턴스가 제공되면 해당 버퍼 위에 Typed 배열 뷰가 생성된다.
      선택적으로 byteOffset을 지정하여 시작 위치를 지정할 수 있으며(기본값은 0),
      길이를 지정하여(기본값은 버퍼의 끝까지) 버퍼의 일부분만 뷰로 만들 수 있다.
  2. new TypedArray(object);
    • 일반 JavaScript 객체가 제공되면, 해당 객체의 속성을 사용하여 TypedArray을 초기화한다.
    • 다만 위의 예시의 경우 Uint8Array 로 배열값을 초기화한 것이기 때문에 특정 인덱스에 255 이상의 값을 넣을 수 없다. 즉, 배열을 복사해서 초기화한다음 가지고 있는 대신 기존의 특성을 그대로 가지고 있는 것이다.
      let arr = new Uint8Array([0, 1, 2, 3]);
      alert( arr.length ); // 4, created binary array of the same length
      alert( arr[1] ); // 1, filled with 4 bytes (unsigned 8-bit integers) with given values
  3. new TypedArray(typedArray);
    • TypeArray 를 TypedArray 에 전달하면서 그 형태를 변환해줄 수도 있다.
    • 여기서 주의할 점은 arr16.buffer 값을 Uint8Array 로 직접 전달한 경우이다.
    • buffer 를 직접 인자로 전달해주었기 때문에 arr16이 바라보고 있는 ArrayBuffer 를 같이 바라보게 되는데, 이 때 해당 길이가 4byte 로 고정이 되어있기 때문에 하나의 요소를 8비트로 바라보는 Uint8Array 기준에서는 요소를 4개로 쪼개서 바라보게 되는 것이다. (by clamping)
    let arr16 = new Uint16Array([1, 1000]);
    let arr8 = new Uint8Array(arr16); // buffer가 새로 생성된다.
    alert( arr8[0] ); // 1
    alert( arr8[1] ); // 232, tried to copy 1000, but can't fit 1000 into 8 bits (explanations below)
    
    arr8 = new Uint8Array(arr16.buffer); // 기존 arr16이 바라보고 있는 ArrayBuffer 를 같이 바라본다.
    alert( arr8.byteLength ); // 4 byte
    alert( arr16.byteLength ); // 4 byte
    alert( arr16.length ); // 2
    alert( arr8.length ); // 4
  4. new TypedArray(length);
    • 숫자 인수 length가 제공되면 해당하는 요소 수를 포함하는 TypedArray가 생성되며, 그 바이트 길이는 length에 TypedArray.BYTES_PER_ELEMENT를 곱한 값이 된다.
      let arr = new Uint16Array(4); // create typed array for 4 integers
      alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer
      alert( arr.byteLength ); // 8 (size in bytes)
  5. new TypedArray();
    • 인수가 없다면 길이가 0인 TypedArray가 생성된다.

TypedArray는 직접 바로 생성할 수 있다. 허나 모든 뷰객체들은 ArrayBuffer 를 기반으로 사용되는 것이기 때문에 자동으로 내장되어 있다. 내장된 값들은 다음과 같이 꺼내 쓸 수 있다.

  • arr.buffer: ArrayBuffer를 참조한다.
  • arr.byteLength: ArrayBuffer의 길이를 나타낸다.

이를 활용해서 다음과 같이 사용해줄 수도 있다.

let arr8 = new Uint8Array([0, 1, 2, 3]);

// another view on the same data
let arr16 = new Uint16Array(arr8.buffer);

TypedArray 들은 다음과 같은 것들이 있으니 참고.

  • Uint8Array, Uint16Array, Uint32Array – for integer numbers of 8, 16 and 32 bits.
    • Uint8ClampedArray – for 8-bit integers, “clamps” them on assignment (see below).
  • Int8Array, Int16Array, Int32Array – for signed integer numbers (can be negative).
  • Float32Array, Float64Array – for signed floating-point numbers of 32 and 64 bits.

2.1 Out-of-bounds behavior

만약 TypedArray에 배열의 범위를 벗어나는 값을 할당하려고 하면, 오류가 발생하지 않는다.
그러나 TypedArray의 용량을 벗어나는 추가적인 비트는 잘린다.

예를 들어 Uint8Array 의 특정 요소에 256를 넣으려고 한다고 가정해보자.

이진 형식으로 256은 8비트를 넘어가 9비트로 표현이 된다.
허나 Uint8Array 는 각 요소가 8비트까지만 보여질 수 있기 때문에 그 범위가 0~255 까지이다.

즉, 9비트로 표현되는 256은 그 범위를 넘어가게 되고 값은 clamping 되어 저장된다.
clamping 될 때 가장 오른쪽 8비트만 저장되기 때문에 256 의 경우 100000000 → 00000000 만 저장되어 0이 되게 된다. 다음은 예시이다.

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000 (binary representation)

uint8array[0] = 256;
uint8array[1] = 257;

alert(uint8array[0]); // 0
alert(uint8array[1]); // 1

 

3. TypedArray methods

TypedArray는 일반적인 배열 메서드를 가지고 있지만, 몇 가지 중요한 예외가 있다.

  • 사용가능한 배열 메서드
    • iterate
    • map
    • slice
    • find
    • reduce
  • 사용불가능한 배열 메서드
    • splice
    • concat

그 외에 추가적으로 사용가능한 메서드가 2개 있다.

  • arr.set(fromArr, [offset]) : fromArr 의 모든 요소를 오프셋 위치부터 arr 로 복사한다.
  • arr.subarray([begin, end]) : 시작부터 끝까지의 동일한 유형의 새로운 뷰를 생성한다. slice 메소드와 유사하지만, 복사하지 않고 새로운 뷰를 만들어 반환한다. splice 의 대용이라고 볼 수 있을 것 같다.

 

4. DataView

DataView 는 TypedArray 와는 다르게 ArrayBuffer 를 바라보는 타입없는 뷰이다.

이를 통해 어떤 형식의 데이터든지 어떤 오프셋에서도 엑세스할 수 있다.

  • TypedArray : 생성자가 형식을 지정, 모든 배열의 값은 동일 타입을 가지고 있으며, 특정 요소는 arr[i] 형태로 접근가능
  • DataView : 특정 데이터에 접근하기 위해서는 .getUint8(i) 이나 .getUint16(i) 와 같은 메소드를 사용해야 한다.

사용되는 형태는 아래와 같다.

new DataView(buffer, [byteOffset], [byteLength])
  • buffer – 기본 ArrayBuffer 이다. TypedArray 와 달리 DataView 는 자체적으로 버퍼를 생성하지 않는다. 때문에 buffer 를 직접 지정해주어야 한다.
  • byteOffset – 뷰의 사직 바이트 위치이다.
  • byteLength – 뷰의 바이트 길이이다.
// binary array of 4 bytes, all have the maximal value 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// get 8-bit number at offset 0
alert( dataView.getUint8(0) ); // 255

// now get 16-bit number at offset 0, it consists of 2 bytes, together interpreted as 65535
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)

// get 32-bit number at offset 0
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)

dataView.setUint32(0, 0); // set 4-byte number to zero, thus setting all bytes to 0

TypedArray 에 비해 Dataview 를 사용하는 경우가 어떤경우일까?

DataView는 동일한 버퍼에 혼합된 형식의 데이터를 저장할 때 유용하다.
예를 들어, 각 쌍이 16비트 정수와 32비트 부동 소수점으로 구성된 시퀀스를 저장하는 경우,
DataView는 이러한 데이터를 쉽게 액세스할 수 있도록 한다.

 

5. 정리

  1. ArrayBuffer: 고정 길이의 연속적인 메모리 영역에 대한 참조로, 핵심 객체
  2. TypedArray: ArrayBuffer의 view로, 다양한 유형이 있음
    • Uint8Array, Uint16Array, Uint32Array: 8, 16, 32비트의 부호 없는 정수를 위한 것.
    • Uint8ClampedArray: 8비트 정수를 '클램프'하여 할.
    • Int8Array, Int16Array, Int32Array: 부호 있는 정수를 위한 것.
    • Float32Array, Float64Array: 32비트와 64비트의 부호 있는 부동 소수점 수를 위한 것.
  3. DataView: 형식을 지정하기 위해 메서드를 사용하는 view. 예를 들어, getUint8(offset)와 같은 메서드를 사용.
  4. ArrayBufferView: 모든 종류의 view에 대한 용어.
  5. BufferSource: ArrayBuffer나 ArrayBufferView에 대한 용어. 이 용어는 "모든 종류의 이진 데이터"를 의미.

이러한 용어들은 이진 데이터를 다루는 메서드와 관련하여 사용되며, 주어진 데이터를 적절히 처리하기 위한 용도로 사용된다. ArrayBuffer와 TypedArray, DataView는 이진 데이터를 다루는데 있어 중요한 개념들이다.

99. 과제

주어진 Uint8Array의 배열을 하나의 배열로 연결하여 반환하는 함수 concat(arrays)를 작성하십시오.

function concat(arrays) {
    // ...your code...
}

let chunks = [
  new Uint8Array([0, 1, 2]),
  new Uint8Array([3, 4, 5]),
  new Uint8Array([6, 7, 8])
];

console.log(Array.from(concat(chunks))); // 0, 1, 2, 3, 4, 5, 6, 7, 8

console.log(concat(chunks).constructor.name); // Uint8Array

 

- 풀이 
  ㄴ ArrayBuffer 는 Array 와 다르게 고정된 길이를 가지고 있음
  ㄴ 즉, 처음 정의된 만큼만의 공간을 메모리 할당받기 때문에 처음 length 계산 후 생성 필요
  ㄴ 그 후 TypedArray 의 method 인 set 을 활용해서 복사할 array 의 offset 계산후 값을 넣어준다.

function concat(arrays) {
    // create new Uint8Array with length that sum of all arrays' length
    const newArrayLength = arrays.reduce((acc, cur) => acc + cur.length, 0);
    const newArrays = new Uint8Array(newArrayLength);

    // copy arrays' array to new Uint8Array
    let newOffset = 0;
    arrays.forEach(array => {
      newArrays.set(array, newOffset);
      newOffset += array.length;
    });

    return newArrays;
}