1P by GN⁺ 10시간전 | ★ favorite | 댓글 1개
  • Web Streams 표준은 브라우저와 서버 간 일관된 데이터 스트리밍을 위해 설계되었지만, 현재는 복잡성과 성능 한계로 인해 개발자 경험이 저하되고 있음
  • 기존 API는 락(lock) 관리, BYOB, 백프레셔(backpressure) 등 설계적 제약으로 인해 사용성과 구현 모두에서 불필요한 부담을 초래함
  • Cloudflare는 비동기 반복(async iteration) 기반의 새로운 스트림 모델을 제안하며, 이 방식은 2배에서 최대 120배 빠른 성능을 보임
  • 새 API는 단순한 async iterable 구조, 명시적 백프레셔 정책, 동기/비동기 병행 지원을 통해 효율성과 일관성을 높임
  • 이 접근은 Node.js, Deno, Bun, 브라우저 등 모든 런타임에서 통합적 스트리밍 모델을 가능하게 하며, 향후 표준 논의의 출발점이 될 수 있음

Web Streams의 구조적 한계

  • WHATWG Streams 표준은 2014~2016년에 개발되어 브라우저 중심으로 설계되었으며, 당시 async iteration이 존재하지 않아 별도의 reader/writer 모델을 도입함
    • 이로 인해 락 관리, 복잡한 읽기 루프, BYOB 버퍼 처리 등 불필요한 절차가 생김
  • 락(locking) 모델은 스트림을 독점적으로 점유해 병렬 소비를 막으며, releaseLock() 누락 시 스트림이 영구적으로 잠기는 문제가 발생함
  • BYOB(Bring Your Own Buffer) 기능은 메모리 재사용을 목표로 했지만, 복잡한 버퍼 분리·전송 모델로 인해 실제 활용도가 낮고 구현 난이도가 높음
  • 백프레셔(backpressure) 는 이론상 지원되지만, desiredSize 값이 음수여도 enqueue()가 성공하는 등 실제 제어가 불가능한 구조
  • read() 호출마다 Promise 생성이 강제되어, 고빈도 스트리밍에서는 성능 저하와 GC 부하를 유발함

실무에서 드러난 문제들

  • fetch() 응답 본문을 소비하지 않으면 연결 풀 고갈이 발생하며, tee() 사용 시 무제한 메모리 버퍼링이 생김
  • TransformStream읽기 준비 여부와 무관하게 즉시 처리되어, 느린 소비자 환경에서 버퍼 폭증을 초래함
  • 서버사이드 렌더링(SSR)에서는 수천 개의 작은 청크 처리로 인한 GC 쓰레싱이 발생해 성능이 급감함
  • 각 런타임(Node.js, Deno, Bun, Workers)은 이를 완화하기 위해 비표준 최적화 경로를 도입했으나, 이로 인해 호환성과 일관성 저하가 발생함
  • Web Platform Tests는 70개 이상의 복잡한 테스트 파일을 요구하며, 이는 과도한 내부 상태 관리와 비직관적 동작의 결과임

새로운 스트림 API 설계 원칙

  • 스트림은 단순한 async iterable로 정의되어, for await...of로 직접 소비 가능
  • Pull-through 변환을 채택해 소비자가 데이터를 요청할 때만 처리 수행
  • 명시적 백프레셔 정책(strict, block, drop-oldest, drop-newest) 을 제공해 메모리 폭주 방지
  • 배치 청크(Uint8Array[]) 단위로 데이터를 전달해 Promise 생성 비용을 줄임
  • 바이트 단위 전용 처리로 단순화하며, BYOB나 복잡한 컨트롤러 개념 제거
  • 동기(synchronous) 경로 지원으로 CPU 중심 작업에서 Promise 오버헤드를 제거

새로운 API의 예시와 특징

  • Stream.push()로 간단히 writer/readable 쌍 생성, Stream.text()로 전체 텍스트 수집 가능
  • Stream.pull()지연(lazy) 파이프라인을 구성해 소비 시점에만 실행
  • Stream.share()Stream.broadcast()명시적 다중 소비자 관리를 지원
  • Sync/Async 병행 API(Stream.pullSync(), Stream.textSync())로 I/O 없는 연산에서 성능 극대화
  • Web Streams와의 상호 운용을 위해 간단한 어댑터 함수로 변환 가능

성능 비교 및 전망

  • Node.js 기준 벤치마크에서 최대 80~90배, 브라우저에서는 최대 100배 이상 빠른 처리 속도 확인
    • 예: 3단 변환 체인에서 275GB/s vs 3GB/s
  • 성능 향상은 비동기 오버헤드 제거, 배치 처리, pull 기반 설계에서 기인
  • 이 구현은 순수 TypeScript/JavaScript로 작성되었으며, 네이티브 구현 시 추가 향상 가능성 존재
  • Cloudflare는 이 접근을 표준 논의의 출발점으로 제시하며, 개발자 커뮤니티의 피드백을 요청함

결론

  • Web Streams는 당시 제약 속에서 합리적이었지만, 현대 JavaScript의 언어 기능과 개발 패턴에 부합하지 않음
  • 새로운 async iterable 기반 모델은 단순성, 성능, 명시적 제어를 모두 충족하며, 런타임 간 일관된 스트리밍 생태계 구축 가능성 제시
  • Cloudflare는 GitHub의 jasnell/new-streams에서 참조 구현과 문서, 예제 코드를 공개함
  • 목표는 새로운 표준 제정이 아니라, “더 나은 스트림 API”를 논의하기 위한 실질적 출발점 마련
Hacker News 의견들
  • 이 글에서 제안한 API보다 더 나은 Stream 인터페이스를 직접 설계했음
    기존 제안은 async iterator of UInt8Array 형태인데, 나는 next()가 동기 혹은 비동기 결과를 모두 반환할 수 있는 구조를 제안함
    이렇게 하면

    • 기존 구조보다 단일 반복자로 단순하게 순회 가능함
    • 동기 입력에 동기 변환을 적용하면 전체 처리가 동기로 가능해 코드 중복을 줄일 수 있음
    • 불필요한 Promise 생성이 줄어들어 성능 향상이 있음
    • 동시성 제어가 가능해 async iterator의 한계를 극복할 수 있음
      • 네가 제안한 방식이 더 낫다고 하지만, 사실 상대방의 방식이 더 기초적인 원시 형태로서 우수하다고 생각함
        네 방식으로는 그들의 구조를 쉽게 만들 수 없고, 반대로는 가능함
        I/O 중심의 반복자는 T 단위의 청크를 반환해야 버퍼 낭비를 막을 수 있음
      • 제안한 스트림 개념이 흥미롭지만, 그들의 설계는 AsyncIterator 호환성을 전제로 함
        Uint8Array를 사용하는 이유는 OS 수준의 바이트 스트림과 맞추기 위함임
        실제로 C 기반 프로젝트에서도 이런 구조가 가장 효율적이므로, 타입 정보를 가진 프로토콜은 그 위에 쌓이는 형태가 자연스러움
      • Node 24에서 동기 함수 호출과 async 함수 호출의 속도 차이를 마이크로벤치마크로 측정했는데, 약 90배 정도 느림
        예전 버전에서는 105배까지 차이 났음
        async 처리 최적화가 Node 16에서 있었고, 그때 일부 테스트가 깨졌던 기억이 있음
      • Uint8Array<T>라는 타입은 존재하지 않음
        Uint8Array는 단순히 바이트 배열을 표현하는 원시 타입이며, 타입 정보는 프로토콜이 아닌 애플리케이션 레벨에서 다뤄야 함
      • 이 구조는 Clojure의 transducer 개념과 비슷함
        참고: Clojure Transducers 문서
  • Async iterable도 완벽한 해결책은 아님
    Promise와 스택 전환 오버헤드가 커서 작은 단위의 데이터를 다룰 때 성능이 나쁨
    Lit-SSR에서는 이를 해결하기 위해 동기 iterable 안에 thunk를 포함하는 방식을 사용했음
    async 작업이 필요할 때만 thunk를 호출하고 await하도록 하여, SSR 성능을 12~18배 향상시켰음
    다만 Streams API는 이런 취약한 계약 구조를 채택하기 어렵기 때문에, write()writeAsync()처럼 선택적 비동기 처리가 가능한 구조가 이상적이라 생각함

    • 네가 말한 문제를 내 stream iterator가 해결할 수 있음
      동기 generator를 활용한 예시를 GitHub 코드로 공유함
      핵심은 step.value.then(value => this.next(value)) 부분임
    • conartist6의 제안(next(): {done, value: T} | Promise<{done, value: T}>)이 마음에 듦
      2013년의 “Do not unleash Zalgo” 논쟁 이후, MaybeAsync<T> 형태를 피하는 경향이 있었지만
      이 공포가 너무 과장되어 빠르고 유연한 API 설계를 막고 있다고 생각함
      여러 값을 한 번에 끌어오는 유틸리티도 만들 수 있고, generator 속도 문제는 실제로는 크지 않다고 느낌
  • Node.js에서 Web Streams를 다루는 건 고통스러움
    브라우저 중심으로 설계되어 서버 환경에서는 불편함
    단순한 변환에도 transform stream을 감싸야 하고, .pipe()처럼 직관적인 체이닝이 어려움
    Async iterable 접근법이 훨씬 자연스럽고 for-await-of와 잘 어울림
    Web Streams 스펙은 너무 추상화 중심적이라 실용성이 떨어짐

    • Node에서 Web Streams를 실제로 쓰는 사람이 있다는 게 놀라움
      나는 그게 단순히 클라이언트–서버 간 호환성용이라고 생각했음
  • 진짜 이점은 성능뿐 아니라 환경 간 일관성(convergence)
    ReadableStream이 브라우저, Worker, 기타 런타임에서 동일하게 동작하면
    코드 이식성과 backpressure 버그 감소 효과가 큼
    스트림 계층의 표준화는 신뢰성 있는 스트리밍 시스템 구축의 핵심임

    • 맞음, 단순히 성능이 아니라 표준화의 가치가 큼
  • 예전에 Repeater라는 추상화를 만들었음
    Promise 생성자를 async iterable로 옮긴 개념으로, 이벤트를 push/stop으로 제어함
    Repeater 라이브러리는 주간 650만 다운로드를 기록할 정도로 안정적임
    최근에는 streams를 더 선호하지만, tee() 관련 비판은 여전히 유효함
    async iterable을 기본 추상화로 삼는 방향이 옳다고 생각함

    • Repeater의 stop이 함수이자 Promise로 동작하는 게 흥미로웠음
      소스 코드를 보고 나서
      전통적인 패턴과 다르지만, 인체공학적 설계를 위한 의도된 선택일 수도 있다고 생각함
    • 주제와는 다르지만, Konami 코드 예시가 너무 반가웠음
      이메일 서명에도 “Up, Up, Down, Down, Left, Right, Left, Right, B, A”를 쓸 정도로 향수가 깊음
  • 나도 AsyncIterable을 더 간결하게 쓰기 위한 래퍼를 만든 적 있음
    fluent-async-iterator인데,
    Lambda나 CLI 파이프라인에서 소규모 데이터 스트리밍에 유용했음
    지금쯤은 더 나은 API가 나왔길 바랐음

  • ReadableStream.tee()backpressure 동작이 Node.js의 pipe()와 반대라 혼란스러움
    명세서에는 “가장 느린 출력이 속도를 결정해야 한다”고 되어 있는데, 실제 구현은 빠른 쪽이 소비되지 않아도 막힘
    새로운 Stream API처럼 push 기반의 간결한 구조가 더 낫다고 생각함
    Node와 Web Streams는 무한 큐를 두어 동기적으로 res.write()를 남발할 수 있게 하지만,
    이 API는 generator 기반의 yield 흐름을 강제해 더 안전함

  • Node.js에서 undici(fetch) 사용 시 커넥션 풀 고갈 문제가 생기는 건
    가비지 컬렉션 언어의 한계 때문임
    명시적으로 리소스를 닫지 않으면 GC 타이밍에 따라 누수가 발생함
    C++의 RAII(reference counting) 접근이 오히려 더 안전함

  • 리소스 해제 관련해서는 using/await using 패턴이 점점 확산되길 바람
    C#의 using처럼 dispose/disposeAsync를 지원하는 구조를 DB 드라이버에 적용 중임

  • 벤치마크 수치(예: 530GB/s)는 M1 Pro의 메모리 대역폭(200GB/s) 을 초과하므로 신뢰하기 어려움
    구현 품질 관리가 부족한 vibe-coded 벤치마크일 가능성이 높음