ReadableStream<Uint8Array> 가 필요해진다. 빈 파일을 열고, 늘 쓰던 컨트롤러 뼈대를 다시 적기 시작한다. 그러다 스트림 끝에서 바이트를 모아야 하는 상황이 생긴다. 또 유틸리티가 필요하다. 그 결과를 이번에는 Uint8Array 가 아니라 ArrayBuffer 를 기대하는 어딘가에 넘겨야 한다. 또 변환이 필요하다.
어려운 작업은 아니다. 다만 전부 노이즈다.
그래서 그 조각들을 @hsblabs/web-stream-extras 로 묶었다.
계속 반복하던 세 가지 패턴
청크 배열로부터 ReadableStream 만들기
// beforeconst stream = new ReadableStream({ start(controller) { for (const chunk of chunks) controller.enqueue(chunk); controller.close(); },});스트림을 하나의 Uint8Array 로 수집하기
// beforeconst chunks: Uint8Array[] = [];const reader = stream.getReader();while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value);}const result = concatU8Arrays(chunks); // 이것도 직접 써야 했다Uint8Array, ArrayBuffer, 문자열 사이를 변환하기
정확한 뷰와 복사 패턴을 찾느라 계속 시간을 쓰게 되는 작은 차이들이다.
After
import { readableFromChunks, readAllBytes, stringToBinary, binaryToString,} from "@hsblabs/web-stream-extras";
const stream = readableFromChunks([ stringToBinary("hello"), stringToBinary(" world"),]);
const result = await readAllBytes(stream);console.log(binaryToString(result)); // "hello world"설치:
npm install @hsblabs/web-stream-extrasNode.js ≥22와 최신 브라우저에서 동작한다. 런타임 의존성은 없다.
바이너리 변환 파이프라인 만들기
커스텀 transform 단계가 필요할 때 ByteTransformStream 은 TransformStream 바닥 작업을 다시 구현하지 않고 확장할 수 있는 타입 기반 클래스를 제공한다.
import { ByteTransformStream } from "@hsblabs/web-stream-extras";
class UpperCaseStream extends ByteTransformStream { transform(chunk: Uint8Array) { // 바이트를 처리하고 this.push()로 출력한다 }}ByteQueue 는 청크 경계를 넘는 부분 읽기가 필요할 때 내부 버퍼링을 맡는다. encodeBase64Url 과 decodeBase64Url 은 바이트 데이터와 텍스트가 만나는 지점에서 쓴다.
스트림 암호화
암호화는 별도 서브패스로 분리했다. 필요하지 않다면 내려받지 않는다.
import { encryptStream, decryptStream } from "@hsblabs/web-stream-extras/encryption";
const key = crypto.getRandomValues(new Uint8Array(32));
const plaintext = readableFromChunks([stringToBinary("secret payload")]);const encrypted = encryptStream(key, plaintext);const decrypted = decryptStream(key, encrypted);
console.log(binaryToString(await readAllBytes(decrypted))); // "secret payload"두 함수 모두 ReadableStream<Uint8Array> 를 받아 같은 타입으로 돌려주므로 파이프라인의 다른 부분과 자연스럽게 연결된다. 내부의 EncryptionStream 과 DecryptionStream 도 TransformStream 래퍼로 노출되어 직접 써야 할 때 활용할 수 있다.
포맷은 HKDF로 파생한 레코드별 키와 nonce를 쓰는 AES-GCM이다. 스트림을 위한 설계이지 범용 파일 암호화 포맷은 아니다.
마스터 키를 쓰는 패턴
애플리케이션이 별도의 AES-GCM 마스터 키를 관리하고 있고, 암호화된 메타데이터와 함께 스트림별 키를 저장하고 싶다면 webCryptoStream 이 그 패턴을 감싼다.
import { webCryptoStream } from "@hsblabs/web-stream-extras/encryption";
const masterKey = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"],);
const crypto$ = webCryptoStream(masterKey);
const streamKey = await crypto$.createStreamKey();
const encrypted = await crypto$.encrypt(streamKey, plaintext);const decrypted = await crypto$.decrypt(streamKey, encrypted);이미 원시 32바이트 키가 있다면 encryptStream / decryptStream 만으로 충분하다.
하지 않는 것
명확히 적어 둘 만한 항목들:
- 인증 없음 — 제공하는 것은 비밀성이지 신원 확인이 아니다
- 키 저장 없음 — 스트림 키를 어디에 저장할지는 애플리케이션 책임
- 패스워드 기반 키 파생 없음 — PBKDF2 / Argon2는 직접 가져와야 한다
- 사용자 관리 없음 — 이것은 low-level 스트림 유틸리티이지 auth 프레임워크가 아니다
빠르게 부족한 부분을 알려 주려면 issue를 여는 것이 가장 좋다.
hsb.horse