자바스크립트/모던 자바스크립트

모던 자바스크립트, TypedArray (타입이 있는 배열)

Jake Seo 2023. 3. 19. 00:51

자바스크립트 기본 배열의 문제점

  • 자바스크립트의 배열은 컴퓨터 과학에서 정의하는 일반적 배열이 아니다.
  • 자바스크립트의 배열은 특수처리가 된 객체이다.
    • 배열 인덱스, length, Array.prototype 에서 상속한 메서드들을 가진 객체이다.

타입이 있는 배열 (TypedArray)

  • ES2015 에 생겨났다.
  • 파일 읽기/쓰기, 그래픽 작업, 수학 API 등에 쓰인다.

TypedArray 와 기존 배열과의 차이

  • 값이 항상 프리미티브 숫자 값이다.
    • 8비트 정수 혹은 32비트 부동 소수점 등이다.
  • 배열 내부의 모든 원소는 동일한 타입이다.
  • 배열을 한번 만들면 길이를 변경할 수 없다.
  • 지정된 바이너리 양식으로 연속 메모리 버퍼에 저장된다.
  • 중간에 공백이 있을 수 없다.
    • 성긴 배열 (sparse array) 이 아니다.
  • 다른 타입의 배열과 메모리를 공유할 수 있다.
  • 스레드 간에 전송되거나 공유될 수 있다.
    • 브라우저의 웹 워커나 워커 스레드에서 공유될 수 있다.
  • 타입이 있는 배열을 가져올 때 Float64 타입이 아니라면, 형변환이 수반된다.
onst a = [];
a[9] = "nine";
console.log(a);

성긴 배열을 만드는 간단한 예제 코드.

TypedArray 에 대해 오해하면 안되는 점

  • 기존 배열과 마찬가지로 객체이다.
    • 포인터가 있는 미가공 데이터 블록이 아니다.
    • 기존 자바스크립트 배열처럼 엔트리가 아닌 속성을 넣을 수 있다.

TypedArray 의 종류

11 가지가 존재한다.

  • 이름, 값타입, 엔트리 사이즈, 변환 작업 순이다.
  • Int8Array Int8 1 ToInt8
  • Uint8Array Uint8 1 ToUint8
  • Uint8ClampedArray Uint8Clamped 1 ToUint8Clamped
  • Int16Array Int16 2 ToInt16
  • Uint16Array Uint16 2 ToUint16
  • Int32Array Int32 4 ToInt32
  • Uint32Array Uint32 4 ToUint32
  • Float32Array Float32 4 반올림 모드 (IEEE-754-2008)
  • Float64Array Float64 8 변환이 필요하지 않다.
  • BigInt32Array BigInt32 8 ToBigInt32
  • BigUInt64Array BigUInt64 8 ToBigUInt64

Clamped array 에서 Clamp 의 의미

TypedArray 사용법

  • 생성자 혹은 생성자의 of 혹은 from 메서드를 통해 생성한다.
    • 배열 리터럴로 생성할 수 없다.
    • TypedArray 의 타입이 달라도 생성법은 같다.

생성자를 통한 생성 방법

  • new TypedArray(): 길이가 0으로 설정된 배열을 생성한다.
  • new TypedArray(length): 길이 만큼의 엔트리가 있는 배열을 생성한다.
  • new TypedArray(object): array-like 객체나 iterable 객체만 인자로 올 수 있다.
  • new TypedArray(typedArray): TypedArray 의 메모리를 복사하여 생성한다. 이터레이터를 거치지 않는다.
  • new TypedArray(buffer[, start[, length]]): ArrayBuffer 혹은 SharedArrayBuffer 를 이용하여 생성한다.

TypedArray 에 다른 타입을 할당했을 때

  • 다른 타입의 값을 할당하면, 자동으로 변환 함수를 수행한다.
  • 평가된 값이 undefined 일 때는 0 이 할당된다.
const a1 = new Int8Array(3);
a1[0] = 1;
a1[1] = "2";
a1[2] = 3;
console.log(a1); // Int8Array(3): [1, 2, 3]

const a2 = Int8Array.of(1, 2, "3");
console.log(a2); // Int8Array(3): [1, 2, 3]

const a3 = Int8Array.from({ length: 3, 0: 1, 1: "2" });
console.log(a3); // Int8Array(3): [1, 2, 0]

const a4 = Int8Array.from([1, 2, 3]);
console.log(a4); // Int8Array(3): [1, 2, 3]

TypedArray 에 범위를 벗어나는 숫자를 입력했을 때

  • 범위를 초과하는 숫자를 입력했을 때 반응은 타입마다 조금씩 다르다.

부동 소수점 배열에 할당하는 경우

  • Float64 의 경우엔 자바스크립트 Number 와 동일하기 때문에 변화가 없다.
  • Float32 의 경우엔 IEEE-754-2008 사양의 "가장 가까운 값으로 반올림, 짝수에 연결" 모드를 사용하여 Float32 로 변환된다.

일반 정수 배열에 할당하는 경우 1: 범위를 초과하는 경우

  • 최대 값이나 최소 값 범위를 넘어서는 경우
  • 래핑이 일어나서 Int8 범위의 값으로 변경된다.
const i1 = Int8Array.of(127, -128, 128, -129);
console.log(i1); // Int8Array(4) [127, -128, -128, 127]
  • 127, -128 은 범위 내에 있는 값이므로 그대로 표현된다.
  • 1281000 0000 이고, 부호 비트가 1(음수) 이라서 1의 보수를 취하면 0111 1111 이 되고 2의 보수를 취하면 1000 0000 이 되어 -128 인 것을 확인할 수 있다.

1의 보수와 2의 보수를 쉽게 이해할 수 있는 관련 포스팅

const i2 = Int8Array.of(500, -500);
console.log(i2); // Int8Array(2) [-12, 12]
  • 500 은 2진수로 바꾸면 0001 1111 0100 이 된다.
    • 우측 끝에서 8비트만 남기면 1111 0100 이 된다.
    • 1111 0100 에서 1의 보수를 취하면 0000 1011 이 된다.
    • 0000 1011 에서 2의 보수로 만들면 0000 1100 이 된다.
    • -12 가 되는 것이다.
  • -500 은 2진수로 바꾸면 0010 0000 1100 이 된다.
    • 8비트만 남기면 0000 1100 이 된다.
    • 12 가 되는 것이다.

일반 정수 배열에 할당하는 경우 2: 소수점을 넣는 경우

const a = new Uint8Array(1);
a[0] = -25.4;
console.log(a[0]); // 231
  • -25.4 를 넣으면 먼저 소수점에 대한 부분이 날아간다.
  • -25 는 8bit 로 표현하면 128 - 25 = 103 이라 -128 + 103 을 해주면 될 것이다.
  • 1110 0111-25 일 것이다.
  • 그런데 이번엔 Int8Array 가 아니라 Uint8Array 여서 1110 0111231 이 된다.

ArrayBuffer: TypedArray 에 사용되는 저장소

  • ArrayBuffer 는 모든 타입의 TypedArray 를 품을 수 있다.
  • 비트를 저장해둘 수 있고, 몇비트씩 짤라서 볼 것이냐에 따라 TypedArray 가 보는 관점이 되는 것이다.
  • 단, 데이터에 직접 접근할 수는 없고 TypedArray 혹은 DataView 를 통해서만 접근 가능하다.

ArrayBuffer 를 이용해 TypedArray 공간 할당해보기

const arrayBuffer = new ArrayBuffer(20);
const intArray = new Int32Array(arrayBuffer);
console.log(arrayBuffer.byteLength); // 20
console.log(intArray.length); // 5
  • new ArrayBuffer(20) 는 20바이트의 ArrayBuffer 를 생성한다.
  • new Int32Array(arrayBuffer) 는 20바이트의 ArrayBuffer 를 이용해 생성된다.
    • 32 비트는 4 바이트고, 총 5개의 공간이 있는 Int32Array 배열이 된다.
const intArray = new Int32Array(5);
console.log(intArray.length); // 5
  • 위의 코드와 같다.

적합하지 않은 공간을 할당한다면?

  • 적합하지 않은 공간이 1개의 공간에 4바이트가 드는데 총 공간이 7바이트로 떨어지거나 하는 경우이다.
  • 배열 1개도 2개도 될 수 없다.
const arrayBuffer = new ArrayBuffer(7);
const intArray = new Int32Array(arrayBuffer);

/*
Uncaught RangeError: byte length of Int32Array should be a multiple of 4
    at new Int32Array (<anonymous>)
    at <anonymous>:2:18
*/
  • 에러가 발생한다.
  • 이러한 경우를 방지하기 위해 TypedArray 에는 BYTES_PER_ELEMENT 라는 속성을 제공한다.
const arrayBuffer = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 5);
const intArray = new Int32Array(arrayBuffer);
  • 보통은 이렇게 코드를 작성할 일은 거의 없고 어떠한 사정에 의해 ArrayBuffer 를 따로 작성하는 경우에 필요하다.

PNG 확장자인지 체크하는 로직 만들어보기

  • 읽은 파일의 데이터를 8바이트씩 읽어 PNG_HEADER 패턴인지 확인한다.
  • FileReader 는 범용적이어서 몇비트 단위로 접근할지 미리 정해두지 않는다.
const PNG_HEADER = Uint8Array.of(
  0x89,
  0x50,
  0x4e,
  0x47,
  0x0d,
  0x0a,
  0x1a,
  0x0a
);

const isPNG = (byteData) =>
  byteData.length >= PNG_HEADER.length &&
  PNG_HEADER.every((b, i) => b === byteData[i]);

const show = (msg) => {
  const p = document.createElement("p");
  p.appendChild(document.createTextNode(msg));
  document.body.appendChild(p);
};

document
  .getElementById("file-input")
  .addEventListener("change", ({ target }) => {
    const file = target.files[0];

    if (!file) {
      return;
    }

    const fr = new FileReader();
    fr.readAsArrayBuffer(file);
    fr.onload = () => {
      const byteData = new Uint8Array(fr.result);
      show(`${file.name} ${isPNG(byteData) ? "is" : "is not"} a PNG file.`);
    };
    fr.onerror = (error) => {
      show(`File read failed: ${error}`);
    };
  });

DataViewArrayBuffer 접근하기

  • ArrayBuffer 에 접근하는 여러 방식을 제공한다.
  • 빅 엔디언 혹은 리틀 엔디언 방식을 지정하여 읽어낼 수도 있다.
    • dv.getUint32(0, true) 처럼 두번째 인자가 truelittleEndian 으로 읽는 것이다.
const PNG_HEADER1 = 0x89504e47;
const PNG_HEADER2 = 0x0d0a1a0a;
const TYPE_IHDR = 0x49484452;

const isPNG = (byteData) =>
  byteData.length >= PNG_HEADER.length &&
  PNG_HEADER.every((b, i) => b === byteData[i]);

const show = (msg) => {
  const p = document.createElement("p");
  p.appendChild(document.createTextNode(msg));
  document.body.appendChild(p);
};

document
  .getElementById("file-input")
  .addEventListener("change", ({ target }) => {
    const file = target.files[0];

    if (!file) {
      return;
    }

    const fr = new FileReader();
    fr.readAsArrayBuffer(file);
    fr.onload = () => {
      const dv = new DataView(fr.result);

      if (
        dv.byteLength >= 24 &&
        dv.getUint32(0) === PNG_HEADER_1 &&
        dv.getUint32(4) === PNG_HEADER_2 &&
        dv.getUint32(12) === TYPE_IHDR
      ) {
        const width = dv.getUint32(16);
        const height = dv.getUint32(20);
        show(`${file.name}은 ${width} x ${height} 픽셀`);
      } else {
        show(`${file.name}은 PNG file 이 아니다.`);
      }
    };
    fr.onerror = (error) => {
      show(`File read failed: ${error}`);
    };
  });

ArrayBuffer 의 공유

  • ArrayBuffer 는 모든 TypedArray 에서 공통으로 사용 가능하기 때문에 공유도 가능하다.

겹침 없는 공유

  • 겹침 없는 공유는 별다른 문제가 없다.
const buf = new ArrayBuffer(20);
const bytes = new Uint8Array(buf, 0, 8);
const words = new Uint16Array(buf, 8);

console.log(buf.byteLength); // 20 바이트
console.log(bytes.length); // 8 바이트
console.log(words.length); // (16 bit * 6) -> 12 바이트

겹침 있는 공유

  • 겹침 있는 공유는 서로를 침범하므로 주의해야 한다.
  • 아래 코드는 Uint8Array 의 배열이 각각 리틀 엔디언의 상위 바이트 하위 바이트를 가리키고 있음을 인지해야 한다.
const buf = new ArrayBuffer(12);
const bytes = new Uint8Array(buf);
const words = new Uint16Array(buf);
console.log(words[0]); // 0
bytes[0] = 1;
bytes[1] = 2;
console.log(bytes[0]); // 1
console.log(bytes[1]); // 1
console.log(words[0]); // 513 -> 리틀 엔디언 사용

TypedArray 의 메서드

  • 배열의 길이가 고정이라 배열의 길이를 변화시키는 메서드는 없다.
    • pop(), push(), shift(), unshift(), splice()
  • 중첩된 배열이 포함되지 않으므로 아래 메서드도 없다.
    • flat(), flatMap(), concat()

map(), slice(), filter() 메서드

  • map(), slice(), filter() 는 지원하지만 같은 타입의 배열만 반환한다.
const a1 = Uint8Array.of(50, 100, 150, 200);
const a2 = a1.map((v) => v * 2);
console.log(a2); // Uint8Array(4) [50, 100, 150, 200]

TypedArray.prototype.set() 메서드

  • setter 와 비슷한 역할을 한다.
  • 배열 내용과 길이를 주면 set() 된다.
const source = new Uint8Array([1, 2, 3, 4, 5]);
const dest = new Uint8Array(5);
dest.set(source);

console.log(dest); // Output: Uint8Array [ 1, 2, 3, 4, 5 ]

TypedArray.prototype.subarray() 메서드

  • 메모리를 공유하는 하위 TypedArray 를 생성한다.
  • beginend 를 인자로 받아 범위를 지정 가능하다.
  • 아래 예제에서 값을 바꾸니 공유하는 메모리 값이 같이 바뀐 것을 알 수 있다.
const wholeArray = Uint8Array.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
const firstHalf = wholeArray.subarray(0, 5);
console.log(wholeArray); // Uint8Array(10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(firstHalf); // Uint8Array(5) [0, 1, 2, 3, 4]
firstHalf[0] = 100;
console.log(wholeArray); // Uint8Array(10) [100, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(firstHalf); // Uint8Array(5) [100, 1, 2, 3, 4]
반응형