타입스크립트의 ArrayBuffer

타입스크립트의 ArrayBuffer

이 글에서는 타입스크립트의 ArrayBuffer에 대해 설명합니다.

타입스크립트의 ArrayBuffer에 대해 기초부터 실전 기술까지 단계별로 설명하겠습니다.

YouTube Video

타입스크립트의 ArrayBuffer

ArrayBuffer는 바이너리 데이터를 위한 '원시 메모리 영역'을 나타내는 내장 객체입니다. 고정 길이의 원시 버퍼를 나타내며, 이 위에 TypedArray나 DataView를 덧붙여 읽거나 쓸 수 있습니다.

ArrayBuffer의 기본 개념 및 특징

ArrayBuffer는 고정 길이의 바이트 시퀀스입니다. 생성 시 바이트 단위로 크기를 지정해야 하며, 이후 길이를 변경할 수 없습니다.

1// Create a new ArrayBuffer of 16 bytes.
2const buf = new ArrayBuffer(16);
3console.log(buf.byteLength); // 16
  • 이 코드는 16바이트 크기의 빈 버퍼를 생성합니다. byteLength 속성으로 크기를 확인할 수 있습니다.

TypedArray 및 DataView — 버퍼를 조작하기 위한 뷰

ArrayBuffer는 데이터를 읽거나 쓸 수 있는 기능이 없습니다. 따라서 실제 연산은 TypedArrayDataView를 통해 이루어지며, 데이터에 접근할 때 정수나 부동 소수점과 같은 타입 및 엔디안을 지정할 수 있습니다.

1// Create a buffer and a Uint8Array view over it. Then write bytes and read them.
2const buffer = new ArrayBuffer(8);
3const u8 = new Uint8Array(buffer);
4
5u8[0] = 0x41; // 'A'
6u8[1] = 0x42; // 'B'
7console.log(u8); // Uint8Array(8) [ 65, 66, 0, 0, 0, 0, 0, 0 ]
  • Uint8Array는 바이트 단위 배열 뷰이며, 일반 배열처럼 접근할 수 있습니다. 동일 ArrayBuffer에 여러 뷰를 올리면 읽기/쓰기 메모리를 공유합니다.

DataView: 임의 경계와 엔디안 처리에서의 읽기/쓰기

DataView는 바이트 단위로 세밀하게 값을 읽고 쓸 수 있으며, 리틀 엔디안 또는 빅 엔디안 지정이 가능합니다.

 1// Using DataView to write/read multi-byte values with endianness control.
 2const buf2 = new ArrayBuffer(8);
 3const view = new DataView(buf2);
 4
 5// Write a 32-bit integer (little-endian)
 6view.setInt32(0, 0x12345678, true);
 7
 8// Read it back as little-endian and big-endian
 9const little = view.getInt32(0, true);
10const big = view.getInt32(0, false);
11console.log(little.toString(16)); // "12345678"
12console.log(big.toString(16));    // "78563412"
  • DataView는 메모리 오프셋을 지정하여 값을 읽고 쓸 수 있으므로, 네트워크 바이트 순서 처리가 필요한 프로토콜 구현에 적합합니다.

문자열과 ArrayBuffer 간의 변환 (TextEncoder / TextDecoder)

텍스트를 바이너리로 변환하거나 그 반대로 변환하려면 TextEncoderTextDecoder를 사용하세요.

 1// Convert string -> ArrayBuffer and back using TextEncoder/TextDecoder.
 2const encoder = new TextEncoder();
 3const decoder = new TextDecoder();
 4
 5// Unicode escape sequences
 6const str = "\u3053\u3093\u306B\u3061\u306F";
 7const encoded = encoder.encode(str); // Uint8Array
 8console.log(encoded); // Uint8Array([...])
 9
10// If you need an ArrayBuffer specifically:
11const ab = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
12console.log(ab.byteLength); // bytes length of encoded text
13
14// Decode back
15const decoded = decoder.decode(encoded);
16console.log(decoded);
  • TextEncoder.encodeUint8Array를 반환하므로, ArrayBuffer가 필요하다면 .buffer 속성을 참고해야 합니다. slice 메서드를 사용하면 오프셋과 길이를 지정하여 필요한 데이터를 추출할 수 있습니다.

ArrayBuffer 슬라이스와 복사

slice 메서드는 새로운 ArrayBuffer를 반환합니다. ArrayBuffer는 크기를 변경할 수 없습니다. 크기를 변경해야 할 경우, 새 버퍼를 만들고 데이터를 복사해야 합니다.

 1// Slice an ArrayBuffer and copy to a new sized buffer.
 2const original = new Uint8Array([1,2,3,4,5]).buffer;
 3const part = original.slice(1, 4); // bytes 1..3
 4console.log(new Uint8Array(part)); // Uint8Array [ 2, 3, 4 ]
 5
 6// Resize: create a new buffer and copy existing content
 7const larger = new ArrayBuffer(10);
 8const target = new Uint8Array(larger);
 9target.set(new Uint8Array(original), 0);
10console.log(target); // first bytes filled with original data
  • slice 메서드는 새로운 ArrayBuffer를 반환하므로 효율성이나 참조 공유를 우선시한다면 TypedArraysubarray 메서드를 사용하여 동일한 버퍼를 참조할 수 있습니다.

TypedArray.subarray와 복사의 차이점

TypedArraysubarray는 동일 ArrayBuffer를 참조하는 새로운 뷰를 반환합니다.

1// subarray shares the same underlying buffer; modifying one affects the other.
2const arr = new Uint8Array([10,20,30,40]);
3const viewSub = arr.subarray(1,3); // shares memory
4viewSub[0] = 99;
5console.log(arr); // Uint8Array [10, 99, 30, 40]
  • 공유된 뷰는 복사 비용을 줄일 수 있지만 같은 버퍼를 참조하므로 부작용에 주의해야 합니다.

버퍼 연결하기 (여러 ArrayBuffer 결합하기)

ArrayBuffer는 고정 길이이므로, 여러 버퍼를 결합하려면 새로운 ArrayBuffer를 만들고 데이터를 복사해야 합니다.

 1// Concatenate multiple ArrayBuffers
 2function concatBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
 3  const total = buffers.reduce((sum, b) => sum + b.byteLength, 0);
 4  const result = new Uint8Array(total);
 5  let offset = 0;
 6  for (const b of buffers) {
 7    const u8 = new Uint8Array(b);
 8    result.set(u8, offset);
 9    offset += u8.length;
10  }
11  return result.buffer;
12}
13
14const a = new Uint8Array([1,2]).buffer;
15const b = new Uint8Array([3,4,5]).buffer;
16const c = concatBuffers([a, b]);
17console.log(new Uint8Array(c)); // [1,2,3,4,5]
  • 대량의 데이터를 자주 연결해야 한다면 이 코드처럼 전체 크기를 미리 계산하고 한 번만 새 버퍼를 할당하는 것이 더 효율적입니다.

ArrayBuffer를 워커에 전달하기 (Transferable)

브라우저의 postMessage에서는 ArrayBuffer를 "transferable" 객체로 전달할 수 있습니다. 소유권을 복사 없이 이전할 수 있어서 복사 비용을 피할 수 있습니다.

1// Example: posting an ArrayBuffer to a Worker as a transferable object (browser)
2const worker = new Worker('worker.js');
3const bufferToSend = new Uint8Array([1,2,3,4]).buffer;
4
5// Transfer ownership to the worker (main thread no longer owns it)
6worker.postMessage(bufferToSend, [bufferToSend]);
7
8// After transfer, bufferToSend.byteLength === 0 in many browsers (detached)
9console.log(bufferToSend.byteLength); // may be 0
  • postMessage의 두 번째 인자로 전송할 객체들을 배열로 지정하면 ArrayBuffer와 같은 transferable 객체의 소유권을 복사 없이 이전할 수 있습니다.
  • 전송 후에는 원래 쪽의 버퍼가 "detached" 상태가 되어 접근할 수 없게 됩니다. SharedArrayBuffer를 사용하면 여러 스레드에서 동시에 접근할 수 있지만, 보안 요구사항과 환경 제약이 따릅니다.

Node.js에서의 처리 (Buffer와의 상호 변환)

Node.js에서는 Node.js의 바이너리 타입인 BufferArrayBuffer 간의 변환이 가능합니다. 이 기능은 타입스크립트로 브라우저와 Node.js를 모두 타겟팅할 때 유용합니다.

 1// Convert ArrayBuffer <-> Node.js Buffer
 2// In Node.js environment:
 3const ab = new Uint8Array([10,20,30]).buffer;
 4const nodeBuffer = Buffer.from(ab); // ArrayBuffer -> Buffer
 5console.log(nodeBuffer); // <Buffer 0a 14 1e>
 6
 7const backToAb = nodeBuffer.buffer.slice(
 8    nodeBuffer.byteOffset,
 9    nodeBuffer.byteOffset + nodeBuffer.byteLength
10);
11console.log(new Uint8Array(backToAb)); // Uint8Array [10,20,30]
  • Node.js의 Buffer.from(arrayBuffer)는 일반적으로 복사를 생성하지만, 참조 공유가 가능한 경우도 있어 offset에 주의해야 합니다.

성능 고려사항 및 베스트 프랙티스

성능 최적화와 ArrayBuffer의 효율적 관리를 위해서는 몇 가지 중요한 점을 유의해야 합니다. 아래에서 실제로 활용 가능한 베스트 프랙티스를 정리해 설명합니다.

  • 불필요한 복사 횟수 줄이기 대용량 바이너리 처리 시에는 subarray(공유 뷰) 또는 transferable을 활용하여 복사를 줄이세요.

  • 대용량 버퍼는 한 번에 할당하기 작은 버퍼를 반복해서 할당하면 오버헤드가 증가합니다. 가능하다면, 큰 버퍼를 한 번에 할당하고 필요할 때 부분적으로 사용하는 것이 좋습니다.

  • 엔디안 방식을 명확하게 지정하세요 멀티바이트 값을 다룰 때는 DataView를 이용해 엔디안을 명시적으로 지정하세요. 네트워크 프로토콜에서는 빅 엔디안이 표준인 경우가 많습니다.

주요 활용 예시

ArrayBuffer는 브라우저와 Node.js 모두에서 널리 사용됩니다.

  1. 바이너리 프로토콜 파싱 및 생성 (DataView로 헤더 정보 처리 등)
  2. 이미지‧음성 데이터 등 미디어 처리 (fetch(...).then(res => res.arrayBuffer())의 활용)
  3. WebAssembly의 공유 메모리 (Wasm 메모리는 ArrayBuffer 기반 동작)
  4. 복잡한 처리를 Worker로 위임 (ArrayBuffer를 transferable로 전달)

아래 코드는 바이너리 데이터를 취득하고 분석하는 예시입니다.

1// Example: fetch binary data in browser and inspect first bytes
2async function fetchAndInspect(url: string) {
3  const resp = await fetch(url);
4  const ab = await resp.arrayBuffer();
5  const u8 = new Uint8Array(ab, 0, Math.min(16, ab.byteLength));
6  console.log('first bytes:', u8);
7}
  • 이 코드는 브라우저에서 임의의 URL의 바이너리 데이터를 가져와 처음 몇 바이트를 표시합니다. 해당 코드는 fetch API로 가져온 데이터를 ArrayBuffer로 로드해서, Uint8Array로 앞 16바이트를 확인합니다.

요약

ArrayBuffer는 원시 메모리를 나타내며, TypedArray와 DataView로 효율적인 바이너리 작업이 가능합니다. 불필요한 복사를 피하고 엔디안을 명시적으로 지정하는 설계로 안전하고 고성능의 바이너리 처리를 달성할 수 있습니다.

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

YouTube Video