타입스크립트에서의 TypedArray

타입스크립트에서의 TypedArray

이 글은 타입스크립트에서의 TypedArray를 설명합니다.

TypeScript에서 TypedArray에 대해 실제 예시를 포함하여 설명하겠습니다.

YouTube Video

타입스크립트에서의 TypedArray

TypedArray이진 데이터를 효율적으로 처리하기 위한 메커니즘입니다. 이는 대용량 이미지 데이터, 네트워크 바이트 스트림, WebGL용 수치 배열 등 저수준 이진 데이터 연산에 특히 유용합니다.

ArrayBuffer 생성 방법

ArrayBuffer는 고정 길이의 바이트 영역을 나타냅니다. 먼저 버퍼를 생성하고 크기와 바이트 길이를 확인합니다.

1// Create an ArrayBuffer of 16 bytes
2const buffer: ArrayBuffer = new ArrayBuffer(16);
3console.log("buffer.byteLength:", buffer.byteLength); // 16
  • 이 코드는 16바이트의 빈 영역을 생성합니다. ArrayBuffer 자체는 읽기/쓰기 기능을 가지고 있지 않으므로, TypedArrayDataView를 통해 접근합니다.

TypedArray의 종류

다음과 같이 다양한 TypedArray 유형이 있습니다. 각 유형은 처리하는 데이터 타입과 크기에 따라 다릅니다.

TypedArray 데이터 타입 비트 크기
Int8Array 8비트 정수 8비트
Uint8Array 부호 없는 8비트 정수 8비트
Uint8ClampedArray 클램프된 부호 없는 8비트 정수 8비트
Int16Array 16비트 정수 16비트
Uint16Array 부호 없는 16비트 정수 16비트
Int32Array 32비트 정수 32비트
Uint32Array 부호 없는 32비트 정수 32비트
Float32Array 32비트 부동소수점 숫자 32비트
Float64Array 64비트 부동소수점 숫자 64비트

기본 TypedArray들 (Uint8Array, Int16Array, Float32Array 등)

TypedArrayArrayBuffer 위에 '타입이 지정된 뷰'를 만듭니다. 아래는 대표적인 TypedArray를 생성하고 사용하는 예시입니다.

 1// Create a buffer and different typed views over it
 2const buf = new ArrayBuffer(8); // 8 bytes
 3
 4// Create views
 5const u8 = new Uint8Array(buf);    // 8 x uint8
 6const i16 = new Int16Array(buf);   // 4 x int16 (since each int16 is 2 bytes)
 7const f32 = new Float32Array(buf); // 2 x float32 (4 bytes each)
 8
 9console.log("Uint8 length:", u8.length);
10console.log("Int16 length:", i16.length);
11console.log("Float32 length:", f32.length);
  • 동일한 ArrayBuffer에 여러 개의 뷰를 생성함으로써, 동일한 메모리를 서로 다른 타입이나 세분화로 읽고 쓸 수 있습니다. 뷰의 length는 요소의 개수이고, byteLength는 바이트 수를 의미합니다.

쓰기 및 읽기 (바이트 단위 연산)

TypedArray에 값을 쓰면 해당 메모리의 바이트가 업데이트됩니다. 다른 뷰로 동일한 버퍼를 읽으면 변화된 내용을 확인할 수 있습니다.

 1// Demonstrate writing via one view and reading via another
 2const buffer2 = new ArrayBuffer(4);
 3const u8view = new Uint8Array(buffer2);
 4const u32view = new Uint32Array(buffer2);
 5
 6u8view[0] = 0x78;
 7u8view[1] = 0x56;
 8u8view[2] = 0x34;
 9u8view[3] = 0x12;
10
11console.log("Uint8 bytes:", Array.from(u8view)); // [120, 86, 52, 18]
12console.log("Uint32 value (platform endianness):", u32view[0]); // value depends on endianness
  • 이 예시에서는 바이트 시퀀스를 쓰고, 동일한 영역을 32비트 정수로 읽어 봅니다. 출력은 실행 환경의 엔디언(바이트 순서)에 따라 달라질 수 있다는 점에 유의하세요.

엔디언(바이트 순서)과 DataView

환경에 따라 달라지는 엔디언 문제를 제어하고 싶을 때는 DataView가 유용합니다. DataView는 읽기 및 쓰기를 할 때 엔디언을 명시적으로 지정할 수 있습니다.

 1// Use DataView to read/write with explicit endianness control
 2const b = new ArrayBuffer(4);
 3const dv = new DataView(b);
 4
 5// write little-endian 32-bit integer
 6dv.setUint32(0, 0x12345678, true);
 7
 8// read as little-endian and big-endian
 9const little = dv.getUint32(0, true);
10const big = dv.getUint32(0, false);
11
12console.log("little-endian read:", little.toString(16)); // "12345678"
13console.log("big-endian read:", big.toString(16));       // different value
  • DataView는 바이트 단위의 세밀한 읽기 및 쓰기를 위한 편리한 메커니즘입니다. 부호 있는/없는 정수, 부동소수점 등 다양한 타입을 지원하고, 엔디언(바이트 순서)도 명시적으로 지정할 수 있어, 바이너리 프로토콜 구현 시 매우 유용합니다.

subarray와 slice의 차이

TypedArraysubarray는 원본 버퍼를 공유하는 뷰를 반환하고, slice는 새로운 복사본을 반환합니다. 어느 쪽을 사용하는지에 따라 성능과 부작용이 다릅니다.

1const base = new Uint8Array([1, 2, 3, 4, 5]);
2
3const shared = base.subarray(1, 4); // shares underlying buffer
4const copied = base.slice(1, 4);    // copies data
5
6shared[0] = 99;
7console.log("base after shared modification:", base); // shows change
8console.log("copied remains:", copied);              // unaffected
  • 공유된 뷰를 수정하면 원본 배열도 변경되어 의도치 않은 부작용이 발생할 수 있습니다. 원본 배열을 안전하게 보존하려면 사전에 slice()를 사용하여 복사본을 생성할 수 있습니다.

버퍼 복사 및 타입 변환 (TypedArray 간 변환)

TypedArray 간에 데이터를 복사하는 방법과, set 메서드를 이용해 일부를 붙여넣는 방법을 설명합니다.

 1// Copy and convert between typed arrays
 2const src = new Float32Array([1.5, 2.5, 3.5]);
 3const dst = new Uint16Array(src.length);
 4
 5// Convert by mapping (explicit conversion)
 6for (let i = 0; i < src.length; i++) {
 7  dst[i] = Math.round(src[i]); // simple conversion rule
 8}
 9console.log("converted dst:", Array.from(dst)); // [2, 2, 4]
10
11// Using set to copy bytes (requires compatible underlying buffer or same element type)
12const src2 = new Uint8Array([10, 20, 30]);
13const dst2 = new Uint8Array(6);
14dst2.set(src2, 2); // copy src2 into dst2 starting at index 2
15console.log("dst2 after set:", Array.from(dst2)); // [0,0,10,20,30,0]
  • 요소 타입이 다르면 단순 복사가 아닌, 반올림이나 범위 검사를 통해 값을 변환해야 합니다. set을 사용하면 동일 요소 타입의 TypedArray 간에 빠르게 복사할 수 있습니다.

실용 예시: 바이너리 프로토콜 파서 (간단 구현)

여기서는 1바이트 타입, 2바이트 데이터 길이, 그리고 뒤에 오는 페이로드로 구성된 고정 형식 이진 데이터를 읽는 간단한 파서 예시를 소개합니다.

 1// Simple binary message parser:
 2// format: [type: uint8][length: uint16 BE][payload: length bytes]
 3function parseMessages(buffer: ArrayBuffer) {
 4  const dv = new DataView(buffer);
 5  let offset = 0;
 6  const messages: { type: number; payload: Uint8Array }[] = [];
 7
 8  while (offset + 3 <= dv.byteLength) {
 9    const type = dv.getUint8(offset);
10    const length = dv.getUint16(offset + 1, false); // big-endian
11    offset += 3;
12    if (offset + length > dv.byteLength) break; // truncated
13    const payload = new Uint8Array(buffer, offset, length);
14    messages.push({ type, payload });
15    offset += length;
16  }
17
18  return messages;
19}
20
21// Example usage
22const buf = new ArrayBuffer(1 + 2 + 3 + 1 + 2 + 2); // two messages
23const dv = new DataView(buf);
24let off = 0;
25// first message: type=1, length=3, payload=[1,2,3]
26dv.setUint8(off, 1); dv.setUint16(off + 1, 3, false); off += 3;
27new Uint8Array(buf, off, 3).set([1, 2, 3]); off += 3;
28// second message: type=2, length=2, payload=[9,8]
29dv.setUint8(off, 2); dv.setUint16(off + 1, 2, false); off += 3;
30new Uint8Array(buf, off, 2).set([9, 8]);
31
32console.log(parseMessages(buf));
  • 이 예제에서는 DataView를 사용하여 헤더를 읽고, Uint8Array로 페이로드 슬라이스를 생성합니다. 엔디언 처리와 버퍼 길이 확인이 중요합니다.

Web API와 TypedArray (예시: 바이너리 데이터 가져오기)

이것은 네트워크 요청으로 받아온 ArrayBufferTypedArray로 처리하는 전형적인 예시입니다.

 1// Example of fetching binary and mapping to typed array
 2async function fetchBinary(url: string) {
 3  const res = await fetch(url);
 4  const ab = await res.arrayBuffer();
 5  const view = new Uint8Array(ab);
 6  // process view...
 7  console.log("received bytes:", view.length);
 8  return view;
 9}
10
11// Usage (call in async context)
12// fetchBinary("/path/to/file.bin");
  • Response.arrayBuffer()로 얻은 ArrayBufferTypedArray에 직접 전달할 수 있습니다. 이미지, 오디오, 커스텀 바이너리 프로토콜 등에 사용됩니다.

성능 팁과 흔한 실수

TypedArray를 사용할 때 알아두면 좋은 '성능 팁' 및 '흔한 실수'를 소개합니다:.

  • 불필요한 복사를 피하세요 대량 데이터를 효율적으로 처리하려면, subarray로 뷰를 분할하거나 여러 뷰에서 같은 ArrayBuffer를 공유하여 불필요한 복사를 줄일 수 있습니다.

  • 엔디언(바이트 순서)에 주의하세요 네트워크 통신이나 파일 포맷에서는 데이터의 순서(바이트 오더)가 지정되어 있는 경우가 많습니다. DataView를 사용하면 읽기/쓰기 시 엔디언을 명확히 지정하여, 의도치 않은 해석 오류를 방지할 수 있습니다.

  • 각 타입의 값 범위에 유의하세요 예를 들어, Uint8은 0~255까지만 표현할 수 있습니다. 음수 값을 입력하면 절단 또는 값의 롤오버가 발생할 수 있으므로, 필요한 변환 규칙을 정의해야 합니다.

  • 가비지 컬렉션 부담을 고려하세요 대용량 ArrayBuffer를 자주 재생성하면 메모리 관리 부담이 증가합니다. 성능이 중요한 상황에서는 기존 버퍼를 최대한 재사용하도록 코드를 설계하는 것이 좋습니다.

요약

TypedArray는 이진 데이터를 빠르고 효율적으로 처리하기 위한 메커니즘입니다. ArrayBuffer로 고정 길이 바이트 영역을 예약하고, 이를 TypedArray 또는 DataView로 원하는 타입별로 읽고 쓸 수 있어 유연한 바이너리 처리가 가능합니다. 사용 사례에 따라 subarray/slice 또는 DataView를 사용할 수 있으며, 엔디언과 복사에 유의하여 구현을 설계해야 합니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video