"Stream 은 빠른 거에 관한 게 아냐. 한 번에 4KB 만 필요한데 메모리에 4GB 안 올리는 거에 관한 거야."
단순한 방식은 예측 가능하게 실패
로그 파일 줄 수 세고 싶어. 명백한 코드:
import { readFile } from 'node:fs/promises';
const text = await readFile('access.log', 'utf-8');
const lines = text.split('\n').length;
console.log(lines);
10MB 로그엔 잘 돌아. 10GB 로그에 시도해. Node 프로세스가 10GB 메모리 할당, 머신이 swap, 스크립트가 out-of-memory 에러로 죽음. 근본 문제: newline 만 세면 되는데 (바이트 하나씩 할 수도 있었음) 파일 전체를 단일 문자열로 요청한 거.
Stream 이 이걸 해결. Stream 은 시간에 걸쳐 너한테 전달되는 청크의 sequence 야, 제한된 메모리 안에서. 청크 N 처리하고, N+1 처리하고, 전체 입력 보유 절대 안 함.
이미 써본 stream 셋
process.stdin— 키보드나 pipe 된 stdin 에서 오는 바이트.process.stdout— 터미널로 나가는 바이트.- 모든
fetch()Response 의 body — 네트워크에서 오는 바이트.
첫 console.log 이후 stream 을 써오고 있었어. 런타임이 숨겨, 보통 생각할 필요 없으니까. 데이터가 커지면 stream 이 안 숨겨져.
멘탈 모델 — 수도꼭지와 양동이
Stream 이 틀린 때
Stream 은 복잡성 더해. 작은 입력엔 — config 파일, 작은 JSON payload, 메모리에 들어간다고 확신하는 거 뭐든 — 그냥 readFile 로 통째로 읽고 파싱. Stream 이 값할 때는:
- 입력이 RAM 보다 클 수 있음.
- 입력 다 읽기 전 출력 생성 시작하고 싶음 (latency win).
- 입력이 무한 또는 open-ended (live tail, 네트워크 소켓, stdin pipe).
- 변환 chain — gzip 압축 풀고, 파싱하고, 필터하고, re-encode — 각 단계가 자연스럽게 streaming.
그 외엔 단순 read-then-process 패턴이 OK 고 더 읽기 쉬워.
Stream 의 두 레벨
Node 는 v0.x 부터 stream 가지고 있었음; API 는 세 메이저 버전 진화. 2026 엔 두 stream 인터페이스 마주쳐:
- Node Stream (
node:stream) — 원조.Readable,Writable,Duplex,Transform. Event 기반 (data,end,error). 대부분fs와http가 이거 씀. - Web Stream (built-in, import 없음) — 더 새거, 브라우저와 spec 일치.
ReadableStream,WritableStream,TransformStream.fetchresponse body,blob.stream()같은 거.
둘 다 작동; 두 에코시스템이 천천히 수렴 중. 모던 Node 가 변환용 Readable.toWeb() 과 Readable.fromWeb() 제공.