이 포스팅은 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 를 활용하면 공원을 걷는 것 만큼이나 간단하다.