Web Streams API 백프레셔로 생산 속도 조절하기
Web Streams의 desiredSize와 writer.ready가 소비자 수요를 어떻게 생산자까지 전달하는지, 큐 크기와 TransformStream에서 주의할 점을 설명합니다.
발행·오류 제보: infostoa · 편집 기준
이 글의 개별 작성자와 전문 검토자 정보는 제공되지 않습니다.
Web Streams API의 백프레셔는 빠른 생산자가 느린 소비자보다 계속 앞서 나가지 않도록 수요 신호를 전달하는 방식입니다. 데이터를 버리거나 작업을 자동으로 빠르게 만드는 기능이 아니라, 큐가 차기 전에 생산을 늦추거나 멈추도록 조정합니다. 응답을 한 번에 메모리에 모으는 대신 청크 단위로 처리할 때 특히 중요합니다.
desiredSize는 큐의 여유를 보여 준다
WritableStream의 writer.desiredSize는 현재 큐의 highWaterMark에서 큐잉 전략이 계산한 크기를 뺀 값입니다. 양수는 설정된 기준 아래 여유가 있다는 뜻이고, 0 이하가 되면 생산자가 다음 청크를 계속 밀어 넣기보다 기다릴 시점일 수 있습니다. 값은 수요 상태를 보여 줄 뿐 총 프로세스 메모리를 보장하거나 소비자가 청크를 이미 처리했다는 뜻은 아닙니다.
생산 루프에서 쓰기 완료를 기다리기
직접 생산 루프를 작성한다면 writer.ready가 해결될 때까지 기다린 뒤 다음 청크를 만들고 writer.write(chunk)를 기다리는 흐름을 고려하세요. 생산자가 await를 무시하고 비동기 작업을 무제한 시작하면 내부 큐 밖에 애플리케이션 작업까지 쌓일 수 있습니다. 입력 읽기, 변환, 출력 쓰기의 동시 작업 수도 제한해야 합니다. backpressure를 받았다고 이미 생성된 큰 버퍼가 자동 해제되지는 않습니다.
writer.ready가 해결되는 것은 큐가 다시 쓰기를 받아들일 여유에 도달했다는 뜻이지, 하위 소비자가 업무 처리를 끝냈다는 뜻은 아닙니다. 반대로 writer.write()의 Promise는 청크가 스트림의 쓰기 과정에서 처리됐을 때 해결됩니다. 데이터베이스 반영이나 원격 작업 완료처럼 스트림 밖의 결과까지 보증하지 않으므로, 그런 완료가 필요하면 애플리케이션의 별도 확인 신호를 기다려야 합니다. 여러 생산자가 같은 writer를 공유한다면 각 루프가 개별적으로 대기하더라도 전체 동시 생성 수가 제한되는지 확인하세요.
pipe와 TransformStream은 수요를 연결한다
ReadableStream.pipeTo(WritableStream)는 파이프 안의 수요와 오류·취소 흐름을 연결하는 표준 경로입니다. TransformStream을 중간에 두면 writable 쪽 입력이 빨라도 readable 쪽 소비가 따라오지 않을 때 생산이 늦춰질 수 있습니다. 여러 단계를 연결할 때 한쪽에서 응답하지 않는 작업을 시작하면 수요 신호가 뒤쪽으로 전달되기 전에 애플리케이션이 별도 큐를 만들어 버릴 수 있으므로 각 단계의 대기열을 살펴보세요.
highWaterMark는 메모리 총량의 상한이 아니다
기본 CountQueuingStrategy는 청크 개수를 세므로 1KB와 20MB 청크를 같은 한 개로 취급할 수 있습니다. 일반 ReadableStream에서는 청크 수 대신 바이트 길이나 업무 단위에 맞춘 size 함수를 둘 수 있습니다. 다만 underlying source에 type: "bytes"를 지정한 바이트 스트림은 청크를 바이트로 계산하므로 strategy의 size를 지정하면 생성 시 RangeError가 발생합니다. 이 경우 highWaterMark만 두고, 별도 ByteLengthQueuingStrategy를 전달하지 마세요. 청크 하나 자체가 매우 크거나 큐 밖에서 생산 작업을 계속 만들면 설정값을 넘는 메모리가 필요할 수 있습니다. 최대 청크 크기와 동시 처리량을 별도로 제한해야 합니다.
전문가 관점: 압력 신호는 경계마다 이어져야 한다
MDN contributors는 backpressure가 pipe 체인을 따라 소비자의 수요를 upstream으로 전달한다고 설명합니다. Google Chrome Developers의 실무 안내는 desiredSize와 writer.ready를 사용해 기다리되, 각 청크의 크기와 전체 대기열을 함께 관리하도록 강조합니다. 따라서 단일 스트림의 highWaterMark만 설정하고 끝내지 말고, 생산자·변환기·소비자 사이의 모든 사용자 정의 큐와 취소 경로를 점검하세요. 이 해석은 메모리 상한을 API가 대신 보장한다는 뜻이 아닙니다.