핵심 정리
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";
createReadStream("./big.log")
.pipe(createGzip())
.pipe(createWriteStream("./big.log.gz"));스트림 흐름
먼저 눈에 들어와야 하는 기본형은 아래입니다.
- 읽기 원천:
Readable - 쓰기 목적지:
Writable - 중간 변환:
Transform - 개념 연결:
pipe() - 운영 연결:
pipeline()
세 역할만 먼저 잡으면 된다
Readable: 만든다Writable: 받는다Transform: 중간에서 바꾼다
이 세 가지를 조합하면 대부분의 스트림 설명이 풀립니다.
- 읽기 시작:
createReadStream() - 쓰기 시작:
createWriteStream() - 단순 연결:
readable.pipe(writable) - 안전한 연결:
await pipeline(...) - 종료 확인:
finished(stream)또는pipeline()반환값
역압
스트림의 진짜 핵심은 backpressure
스트림이 필요한 가장 큰 이유는 "생산 속도"와 "소비 속도"가 다를 수 있기 때문입니다.
- 파일 읽기가 더 빠를 수 있다
- 네트워크 전송이 더 느릴 수 있다
- 압축/암호화 단계가 병목일 수 있다
pipe()는 이런 속도 차이를 자동으로 조절합니다. 그래서 스트림 카드는 메모리 절약 카드이면서 동시에 흐름 제어 카드입니다.
연결 방식
pipe()와 pipeline()은 역할이 다르다
pipe()는 연결 자체는 쉽지만, 에러 정리와 종료 처리가 느슨할 수 있습니다. 프로덕션 쪽으로 갈수록 pipeline() 쪽이 더 안전합니다.
즉:
- 개념 이해/간단 연결:
pipe() - 실제 안전한 연결/에러 정리:
pipeline()
이 경계를 같이 기억해 두는 편이 좋습니다.
import { pipeline } from "node:stream/promises";
await pipeline(
createReadStream("./big.log"),
createGzip(),
createWriteStream("./big.log.gz"),
);즉 "연결은 되었는데 중간 에러 정리는 누가 하나"가 고민되면 pipe()보다 pipeline()으로 넘어가는 편이 맞습니다.
스트림은 메모리 절약보다 흐름 제어 카드에 더 가깝다
큰 파일을 한 번에 안 올리는 이점도 있지만, 더 중요한 건 생산자와 소비자 속도가 다를 때 그 차이를 조절할 수 있다는 점입니다. 그래서 스트림 카드는 파일 크기 카드이면서 동시에 backpressure 카드입니다.
언제 어떤 스트림을 고를까
스트림을 고를 때 볼 점
-
큰 로그/업로드/다운로드 처리
-
HTTP body를 흘려 보내는 경우
-
gzip, crypto, parser처럼 중간 변환이 필요한 경우
-
작으면
readFile -
크거나 길이를 모르면 stream
-
변환 단계가 있으면
Transform -
실제 연결은
pipeline()까지 같이 고려 -
스트림의 핵심은 backpressure
-
메모리 절약보다 생산/소비 속도 조절 감각을 먼저 본다
잘못된 예시는 보통 이쪽입니다.
const data = await readFile("./big.log");
await writeFile("./big.log.gz", gzipSync(data));작은 파일에는 괜찮지만, 큰 파일을 통째로 메모리에 올리면 "스트림을 써야 하는 이유"가 사라집니다.
주의할 점
스트림을 쓴다고 해서 자동으로 "안전하고 빠른" 것은 아닙니다. 에러 전파와 종료 정리를 빼먹으면 오히려 파일 디스크립터나 스트림 객체가 열린 채로 남을 수 있습니다. 그래서 스트림 카드는 항상 pipeline() 카드와 같이 읽는 편이 좋습니다.
참고 링크
1 sources