한국정보기술진흥원
한국정보기술신문
thumbnail

정보기술 ·

자바스크립트 웹 스트림 API는 이제 바뀌어야 한다...클라우드플레어, 대안 설계 공개

발행일
읽는 시간3분 47초

클라우드플레어 엔지니어, 웹 스트림 구조적 결함 지적하며 최대 120배 빠른 대안 제시

[한국정보기술신문] 클라우드플레어의 핵심 엔지니어 제임스 M. 스넬이 현재 웹 플랫폼에서 표준으로 사용되고 있는 자바스크립트 웹 스트림 API의 근본적인 설계 결함을 지적하고, 최대 120배 빠른 성능을 보이는 대안 API 설계를 공개했다. 2026년 2월 27일 클라우드플레어 공식 블로그를 통해 발표된 이 글은 자바스크립트 런타임 생태계 전반에 걸쳐 큰 반향을 불러일으키고 있다.

웹 스트림 표준은 2014년부터 2016년 사이에 개발된 규격으로, 브라우저와 서버 환경 모두에서 스트리밍 데이터를 처리하기 위한 공통 API를 제공하는 것을 목적으로 한다. 이 표준은 클라우드플레어 워커스, Node.js, Deno, Bun 등 주요 자바스크립트 런타임에서 널리 채택되었으며, fetch() API의 기반이 되기도 했다.

10년 전 설계 결정이 낳은 구조적 문제

스넬은 오랜 기간 Node.js와 클라우드플레어 워커스에서 웹 스트림을 직접 구현하고 디버깅하며 얻은 경험을 바탕으로, 현재 표준의 문제가 버그가 아니라 설계 결정 자체에서 비롯된 것임을 강조했다.

그가 지적한 첫 번째 문제는 과도한 의례적 절차이다. 스트림에서 데이터를 읽는 가장 기본적인 작업을 수행하기 위해 개발자는 먼저 getReader()를 통해 리더 객체를 획득하고, 반복적으로 read()를 호출한 뒤, 작업이 끝나면 반드시 releaseLock()으로 잠금을 해제해야 한다. 이러한 복잡한 절차는 async/await 문법이 자리잡기 이전인 2014년 당시 설계가 이루어졌기 때문에 생긴 부산물로, 현재의 for await...of 반복문 같은 자바스크립트 기본 언어 기능과 조화를 이루지 못한다는 평가다.

잠금 모델 역시 심각한 문제로 지목됐다. getReader()를 호출한 뒤 releaseLock()을 잊어버리면 해당 스트림은 영구적으로 잠긴 상태가 되어 더 이상 사용할 수 없게 된다. 잠긴 스트림은 파이프 연결도, 취소도 불가능하다. 이런 상황은 실제 개발 현장에서 빈번하게 발생하는 문제로, 규명하기 어려운 버그를 양산하고 있다.

BYOB(Bring Your Own Buffer) 읽기 방식도 논쟁의 대상이 됐다. 이 방식은 고성능 시나리오에서 메모리 버퍼를 재사용하기 위해 도입됐지만, API가 지나치게 복잡하고 ArrayBuffer 이전 의미론 때문에 오류가 발생하기 쉽다. 실제로 대부분의 개발자는 이 복잡한 방식을 사용하지 않고 기본 읽기 방식으로 회귀하는 경우가 많다.

백프레셔와 성능: 이론과 현실의 괴리

백프레셔는 느린 소비자가 빠른 생산자의 속도를 조절할 수 있도록 하는 중요한 기능이다. 웹 스트림은 이를 공식적으로 지원하지만, 실제 구현에서는 신호가 단순히 권고 수준에 그쳐 강제성이 없다. controller.enqueue()는 desiredSize가 극도로 음수 상태여도 항상 성공하며, 생산자는 이를 무시하고 데이터를 계속 밀어 넣을 수 있다. tee() API는 두 분기 중 하나가 느릴 경우 무제한으로 메모리가 증가하는 문제를 내포하고 있다.

성능 측면에서 웹 스트림은 내부적으로 많은 수의 프로미스 객체를 생성한다. 스트리밍 서버 사이드 렌더링 환경에서는 수천 개의 작은 HTML 조각이 스트림을 통과할 때마다 프로미스와 중간 버퍼 객체들이 생성됐다가 즉시 폐기되는 과정을 반복하면서, 가비지 컬렉션이 전체 CPU 시간의 50% 이상을 차지하는 사례도 보고됐다.

버셀의 말테 우블은 최근 발표한 연구에서 Web streams의 pipeThrough가 초당 630MB를 처리하는 데 반해, Node.js의 기존 pipeline() 방식은 초당 7,900MB를 처리해 약 12배의 성능 차이가 난다고 밝혔다. 이 차이의 원인은 대부분 프로미스와 객체 할당 오버헤드에 있다고 스넬은 분석했다.

클라우드플레어가 제시하는 대안: 이터러블 기반 스트림

스넬이 제안한 대안적 설계의 핵심은 스트림을 자바스크립트 비동기 이터러블로 다루자는 것이다. 리더 획득, 잠금 관리, {value, done} 프로토콜 같은 복잡한 절차 대신, for await...of 반복문으로 자연스럽게 스트림을 소비할 수 있도록 한다.

주요 설계 원칙은 다음과 같다. 첫째, 풀-스루 방식의 변환으로 소비자가 데이터를 요청할 때만 변환이 실행되어 불필요한 중간 버퍼 축적이 발생하지 않는다. 둘째, 명시적인 백프레셔 정책으로 버퍼가 가득 찼을 때의 동작을 엄격 거부, 대기, 오래된 데이터 삭제, 새 데이터 삭제 중에서 명시적으로 선택하도록 강제한다. 셋째, 청크 배치 처리로 단일 청크 대신 Uint8Array 배열로 묶어 비동기 오버헤드를 분산시킨다. 넷째, 동기/비동기 분리 경로를 제공해 I/O가 없는 메모리 내 데이터 처리 시 프로미스 없이 완전 동기 파이프라인을 실행할 수 있도록 한다.

벤치마크: 최대 120배 성능 향상

스넬이 공개한 벤치마크 결과는 인상적이다. 동일한 Apple M1 Pro 환경에서 Node.js 24.x 기준으로 소형 청크(1KB×5,000) 처리 시 대안이 약 13GB/s로 웹 스트림의 4GB/s 대비 약 3배 빠르고, 3개의 변환을 연결한 체인 파이프라인에서는 대안이 약 275GB/s로 웹 스트림의 3GB/s 대비 80~90배 빠른 성능을 보였다. 크롬 브라우저 환경에서도 비동기 반복 처리 시 대안이 110만 ops/s로 웹 스트림의 1만 ops/s 대비 최대 100배 이상의 성능 우위를 기록했다.

스넬은 이 성능 차이가 특별한 최적화 기법 때문이 아니라 순전히 설계 철학의 차이에서 비롯된 것임을 강조했다.

업계 반응과 향후 전망

Node.js 기술 운영위원회 위원인 로버트 나기는 새로운 접근법이 레거시 부담 없이 현대 런타임 환경에 맞는 더 간결하고 성능 높은 스트림 모델의 가능성을 열어준다고 평가했다. Platformatic의 공동 창업자이자 Node.js TSC 의장인 마테오 콜리나도 Node.js의 fetch() 구현에서 스트림 복제 시 발생하는 연결 누수 문제를 언급하며 현 표준의 복잡성이 구현 오류를 유발하기 쉬운 환경을 만든다는 데 동의했다.

스넬은 해당 대안 API의 참조 구현을 깃허브(github.com/jasnell/new-streams)에 공개하고 개발자 커뮤니티의 피드백을 요청했다. 그는 이 제안이 즉각적인 표준 대체를 목표로 하는 것이 아니라, 웹 스트림의 현 상태를 넘어 새로운 스트리밍 기본 개념을 논의하는 출발점이 되기를 바란다고 밝혔다.

웹 스트림 표준이 2014년 당시의 제약 조건 속에서 야심차게 설계된 것임을 인정하면서도, 비동기 반복, 수년간의 프로덕션 경험, 그리고 진화한 자바스크립트 언어 기능을 반영한 더 나은 스트림 API에 대한 논의가 본격화될 것으로 보인다.

한국정보기술신문 정보기술분과 유상헌 기자 news@kitpa.org