toggle menu

[NodeJS] Writable streams, 기초에서 커스터마이징까지

2015. 6. 24. 11:27 Node.js

이 포스팅은 codewinds.com의 Storing data with Node.js writable streams을 번역한 글입니다.




텍스트 파일 쓰기


String은 기본적으로 UTF8인코딩이기 때문에, 가장 간단한 에제는 UTF8 인코딩으로 텍스트 파일을 쓰는 것일 것이다.


var fs = require('fs');
var wstream = fs.createWriteStream('myOutput.txt');

wstream.write('Hello world!\n');
wstream.write('Another line\n');
wstream.end();


만약 다른 인코딩으로 쓰고 싶다면, 간단하게 createWriteStream 옵션을 추가해주거나 아니면 각 write에 인코딩 방식을 지정해주면 된다.


//이렇게 createWriteStream 에 옵션으로 지정하거나,
var options = { encoding: 'utf16le' };
var wstream = fs.createWriteStream('myOutput.txt', options);

//아니면 이렇게 각 write 에서 인코딩을 지정해줄 수도 있다.
wstream.write(str, 'utf16le');




바이너리 파일 쓰기

바이너리 파일을 쓰는 것은 단순히 String 대신에 Buffer를 사용하는 것만 달라진다.



var crypto = require('crypto');
var fs = require('fs');
var wstream = fs.createWriteStream('myBinaryFile');

// 100 바이트의 랜덤 버퍼를 만든다.
var buffer = crypto.randomBytes(100);
wstream.write(buffer);
// 또다른 100 바이트의 랜덤 버퍼를 만들어서 바로 쓴다.
wstream.write(crypto.randomBytes(100));
wstream.end();


이 에제에서는 데이터가 담긴 Buffer를 생성하기 위해서 crypto.createRandomBytes() 를 사용했지만, 바이너리 데이터를 직접 생성하거나 아니면 더 쉽게는 다른 소스에서 읽어 들여서 사용할 수 도 있다.





파일에 쓰기가 완료되는 시점

노드에서 파일 입출력은 비동기 작업이기 때문에 파일이 언제 다 쓰여지는지 알고 싶을 수도 있다. 방법은 간단하다. 스트림이 던져주는 이벤트에 대한 리스너를 설정해두면 된다. "finish" 이벤트는 모든 데이터가 시스템에 다 던져지는 시점을 가리킨다.



var fs = require('fs');
var wstream = fs.createWriteStream('myOutput.txt');
// 노드 0.10 버전부터 완료될 때 finish 이벤트를 던진다.
wstream.on('finish', function () {
    console.log('파일 쓰기 완료');
});
wstream.write('Hello world!\n');
wstream.write('Another line');
wstream.end();


만약 노드 v0.10 이전이라면 아래와 같이 .end() 메소드를 사용해서 쓰기 완료 시점을 파악해야 한다.


var fs = require('fs');
var wstream = fs.createWriteStream('myOutput.txt');
wstream.write('Hello world!\n');
wstream.write('Another line');
//노드 0.10 이전 버전일 경우, end() 에 콜백을 등록해서 쓰기 완료 시점을 파악한다.
wstream.end(function () { console.log('done'); });





커스텀 writable stream 생성하기

만약 데이터를 파일이 아닌 데이터베이스나 다른 스토리지에 저장하는 custom writable stream 이 필요하다면, Streams2 Writable abstract class 를 사용하거나 polyfill 모듈인 readable-stream 을 사용해서 구현할 수 있다. 노드의 스트림이 갖는 모든 기능을 하는 writable stream을 만들기 위해서 해야할 것은 단지 Writable를 상속해서 _write(chunk, encoding, cb) 을 구현하기만 하면 된다.



// 노드 v0.10 이상이라면 Streams2 Writable을 사용하고,
// 그렇지 않으면 polyfill 모듈인 readable-stream을 사용한다.
var util = require('util');
var stream = require('stream');
var Writable = stream.Writable || require('readable-stream').Writable;

function MyStream (options) {
    Writable.call(this, options);
}
util.inherits(MyStream, Writable);

MyStream.prototype._write = function (chunk, enc, cb) {
    // chunk를 저장한 뒤, 마치면 cb을 호출한다.
    cb();
};





메모리에 기록하는 스트림 생성하기

간단한 in-memory datastore 를 작성해보자. 우리는 키로 스트림 생성시에 주어진 이름을 사용하고, 완료될 때까지 계속 데이터를 덧붙일 것이다.



var stream = require('stream');
var util = require('util');
// Node.js Writable 이 없을 경우, polyfill을 로드
var Writable = stream.Writable || require('readable-stream').Writable;

var memStore = { };

/* Writable memory stream */
function WMStrm(key, options) {
    // new operator 없이 사용할 수 있도록
    if (!(this instanceof WMStrm)) {
        return new WMStrm(key, options);
    }
    
    Writable.call(this, options); // init super
    this.key = key; // save key
    memStore[key] = new Buffer(''); // empty
}
util.inherits(WMStrm, Writable);

WMStrm.prototype._write = function (chunk, enc, cb) {
  // 버퍼 형태로 일단 memory store 에 저장
  var buffer = (Buffer.isBuffer(chunk)) ? chunk : new Buffer(chunk, enc); //버퍼면 바로 저장하고 아니면 컨버팅해서 저장
  // 기존 버퍼에 추가
  memStore[this.key] = Buffer.concat([memStore[this.key], buffer]);
  cb();
};


// 위에서 만든 것 테스트
var wstream = new WMStrm('foo');
wstream.on('finish', function () {
    console.log('쓰기 완료');
    console.log('데이터:', memStore.foo.toString());
});
wstream.write('hello ');
wstream.write('world');
wstream.end();





우아하고 사용하기 간단한 Writable stream

별다른 큰 노력없이도 노드의 스트림을 사용하면 바이너리나 텍스트를 파일에 쓸 수 있고, 심지어 완전히 기능하는 커스터마이징된 writable stream을 만드는 것도 Node.js v0.10에서 소개된 새로운 streams2 를 활용하면 공원을 걷는 것 만큼이나 간단하다.


Node.js 관련 포스팅 더보기