"Duplex 는 Readable AND Writable 을 함께 볼트 조인. Transform 은 write 쪽이 read 쪽으로 먹여주는 Duplex. 둘 다 추상적으로 들리는데; 둘 다 네 코드에 매일 나와."
Duplex — 독립적인 두 반쪽
Duplex stream 은 독립적 Readable 쪽과 Writable 쪽 가짐. 쓰는 게 읽는 거에 꼭 영향 안 줘. Canonical 예: 네트워크 소켓. 나가는 바이트 쓰고; 들어오는 바이트 읽고; 두 흐름이 같은 TCP 연결 위에 mux 된 별도 채널.
import { createConnection } from 'node:net';
const sock = createConnection({ host: 'example.com', port: 80 });
// Writable side — send a request
sock.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
// Readable side — read the response
for await (const chunk of sock) {
process.stdout.write(chunk);
}
Duplex 는 양방향 필요한데 개념적으로 무관할 때 중요 — 소켓, WebSocket 연결, child process stdio (부모 시각에선 각 pipe 가 Duplex).
Transform — 입력이 출력 됨
Transform stream 은 *특별한* Duplex: 안으로 쓴 바이트가 변환되고 readable 쪽에서 emit. 같은 stream 객체, 양쪽 끝, 근데 두 쪽이 연결됨. 끊임없이 쓰는 예:
zlib.createGzip()— raw 바이트 쓰고, gzipped 바이트 읽음.zlib.createGunzip()— gzipped 바이트 쓰고, raw 바이트 읽음.crypto.createHash('sha256')— 데이터 쓰고, 끝에 digest 읽음.crypto.createCipheriv(...)— 평문 쓰고, 암호문 읽음.
이 다 compose 돼. file → gzip → encrypt → upload 가 stream 넷 pipe 로 묶인 거고, 입력 크기 무관하게 bounded 메모리.
커스텀 Transform 만들기
transform(chunk, encoding, callback) 함수 받음 — 청크 처리하면 callback() 호출 (옵션으로 this.push(value) 통해 다음 출력). Flush 로직은 옵션 flush(callback).import { Transform } from 'node:stream';
class UppercaseUtf8 extends Transform {
_transform(chunk, _enc, cb) {
this.push(chunk.toString('utf-8').toUpperCase());
cb();
}
}
// or as a one-liner via the Transform constructor
import { Transform } from 'node:stream';
const upper = new Transform({
transform(chunk, _enc, cb) {
cb(null, chunk.toString('utf-8').toUpperCase());
},
});
// Use it
process.stdin.pipe(upper).pipe(process.stdout);패턴: 청크 읽기, 작업, 결과 push, done 신호. 그게 transform 의 90% 의 API 전부.Object-Mode Transform — CSV, JSON, Record
Object mode 의 Transform 이 구조화된 데이터에 깔끔한 파이프라인 짜게 해줘. CSV 파서: 입력 바이트, 출력 row 객체. Line splitter: 입력 바이트, 출력 string (라인당 하나). Enricher: 입력 record 객체, 출력 같은 객체에 추가 필드 채워.
import { Transform } from 'node:stream';
class ParseCsvRow extends Transform {
constructor() { super({ objectMode: true }); this.cols = null; }
_transform(line, _enc, cb) {
const fields = String(line).split(',');
if (!this.cols) { this.cols = fields; cb(); return; }
const obj = Object.fromEntries(this.cols.map((c, i) => [c, fields[i]]));
cb(null, obj);
}
}
여러 Transform Compose
Transform 의 힘은 composition. fileStream → split-by-line → parse-csv → filter-by-condition → write-to-db 가 단계 다섯, 각자 독립, 각자 unit-testable. 단계 3 이 4 다시 안 쓰고 바뀔 수 있어. 그래서 stream 이 메모리만이 아니라 코드 복잡성에서도 scale 해.