본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

Readable와 Writable — 두 기초

~13 min · streams, readable, writable

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Readable은 데이터를 만들어 내고, Writable은 받아서 내보내. Node 스트림의 나머지 형태도 이 두 역할을 조합한 거야."

데이터가 들어오는 Readable

Readable 스트림은 데이터를 작은 덩어리로 나눠 차례로 내보내. fs.createReadStream()process.stdin, HTTP 서버의 요청 객체가 대표적인 예야. 기본적으로는 바이트를 내보내지만 객체 모드에서는 JavaScript 객체도 한 덩어리로 다룰 수 있어.

현재 코드에서는 비동기 순회로 읽는 방법이 가장 단순해:

import { createReadStream } from 'node:fs';

const stream = createReadStream('input.txt', { encoding: 'utf-8' });
for await (const chunk of stream) {
  console.log('got', chunk.length, 'chars');
}

이벤트 방식도 여전히 많이 보여. data는 새 덩어리가 왔을 때, end는 입력이 끝났을 때, error는 읽기에 실패했을 때 발생해. 새 코드는 비동기 순회를 먼저 고려하고, 기존 이벤트 기반 코드와 연결할 때 이 방식을 이해하면 돼.

데이터를 내보내는 Writable

Writable 스트림은 들어온 데이터를 목적지로 보낼 때 사용해. fs.createWriteStream()은 파일로, process.stdout은 터미널로, HTTP 응답 객체는 클라이언트로 데이터를 보내.

import { createWriteStream } from 'node:fs';

const out = createWriteStream('output.txt');
out.write('hello\n');
out.write('world\n');
out.end();

write()true를 돌려주면 내부 버퍼에 여유가 있다는 뜻이야. false라면 소비 속도가 쓰기 속도를 따라가지 못해 버퍼가 찼다는 뜻이고, 이 반환값 자체가 쓰는 쪽에 전달되는 역압 신호야. 이때는 drain 이벤트가 올 때까지 쓰기를 멈춰야 메모리가 끝없이 늘어나지 않아.

바이트 대신 객체를 흘려보낼 때

스트림은 기본적으로 바이트를 다루지만 { objectMode: true }를 주면 JavaScript 값을 하나의 덩어리로 보낼 수 있어. CSV 바이트를 행 객체로 바꾸는 파서, 데이터베이스 행을 차례로 읽는 커서, 이벤트를 객체로 감싼 입력에 유용해. 객체 모드는 바이트 모드보다 부가 비용이 있으므로 정말 객체 단위 처리가 필요할 때 사용하고, 스트림을 만든 뒤 두 모드를 섞지는 마.

Readable이 멈춰 있을 때와 흐를 때

Readable은 소비자가 직접 read()를 호출할 때까지 데이터를 버퍼에 두는 상태와, 데이터가 생길 때마다 소비자에게 내보내는 상태가 있어. data 처리 함수를 붙이거나 resume()을 호출하면 후자의 방식으로 바뀌어.

for await...of를 쓰면 이 차이를 직접 관리할 일이 거의 없어. 현재 덩어리의 처리가 끝난 뒤 다음 값을 요청하므로 소비 속도에 맞춰 자연스럽게 멈췄다가 이어져. 두 상태의 구분은 이벤트 API와 수동 흐름 제어를 섞을 때 특히 중요해.

Readable을 직접 만드는 방법

Readable을 상속해 _read()를 구현할 수 있지만, 대부분은 Readable.from(iterableOrAsyncIterable)이 더 간단해. 배열이나 생성기, 비동기 생성기를 이미 갖고 있다면 그대로 스트림으로 바꿀 수 있어. 상속은 운영체제 장치나 특별한 데이터 원본처럼 읽기 동작을 직접 제어해야 할 때만 선택해.

Pippa의 고백

예전 튜토리얼에서 dataend 이벤트, pause()resume()을 한꺼번에 보고 스트림은 원래 복잡한 줄 알았어. 아빠가 같은 입력을 for await (const chunk of readable)로 읽어 보라고 했지. 동작 원리는 같아도 소비하는 코드는 평범한 반복문처럼 보였어. 그 뒤로 큰 입력은 먼저 비동기 순회로 읽고, 정말 필요한 경우에만 낮은 단계의 흐름 제어로 내려가.

Code

`Readable.from()`으로 기존 순회 대상을 스트림으로 만들기·javascript
// Readable from any iterable — the easy way
import { Readable } from 'node:stream';

// From an array
const nums = Readable.from([1, 2, 3, 4, 5], { objectMode: true });
for await (const n of nums) console.log(n);

// From an async generator
async function* paginated() {
  for (let p = 1; p <= 10; p++) {
    const data = await fetchPage(p);
    yield* data;   // yield each item separately
  }
}
const stream = Readable.from(paginated(), { objectMode: true });
버퍼가 찼을 때 멈추는 수동 쓰기·javascript
// Backpressure-aware writing
import { createWriteStream } from 'node:fs';

const out = createWriteStream('out.txt');

async function pumpLots(source) {
  for await (const chunk of source) {
    if (!out.write(chunk)) {
      // Internal buffer full — wait for drain
      await new Promise(r => out.once('drain', r));
    }
  }
  out.end();
}

// Better: just use pipeline (next lesson) — it handles backpressure for you
// import { pipeline } from 'node:stream/promises';
// await pipeline(source, out);

External links

Exercise

파일을 한 줄씩 내보내는 lineStream(path)를 만들어 봐. createReadStream()readline.createInterface()에 연결하고, 그 결과를 Readable.from(..., { objectMode: true })으로 감싸. 이어서 ERROR가 들어간 줄만 출력하면서 개수를 세어 봐.
Hint
readline만으로도 줄 단위 순회는 가능해. Readable.from()으로 다시 감싸는 이점은 다른 Transform이나 Writable과 이어지는 더 큰 스트림 처리 흐름에 넣기 쉬워진다는 데 있어. 단독 소비라면 굳이 한 겹 더 만들 필요는 없어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.