io.js의 v2.0이 나왔습니다.

io.js logo

참 오래간만의 io.js 항목의 글입니다만, 최근에 io.js의 v2.0이 나왔습니다. 변경사항을 간추려서 설명하겠습니다. 쓰다 보니 길어졌기 때문에, 요약만 읽고 싶은 분은 마지막의 정리만 읽으셔도 됩니다.

io.js는 변화가 워낙 많은 편이라, v1.0에서 v2.0으로 되면서 여러 기능이 추가되고 있는데, 모르는 사람도 많지 않을까? 하고 생각합니다.

이번에는 v2.0의 단순한 변경 사항뿐만 아니라, v1.0부터 지금까지 추가된 기능을 요약해 소개해보려고 합니다.

Stream Simpler Construction (v1.2.0~)

Stream의 작성이 간단해졌습니다. 지금까지 Stream을 만들기 위해서는, 목적의 Stream을 상속해서 TransformStream이라면 _transform과 같은 메서드를 확장해서 구현할 필요가 있었습니다.

이를 더 간단히 하기위해 through2로 대표되는 헬퍼 라이브러리가 있었지만, 단순하게 설명하면 이 through2가 없어도 io.js에서는 Stream 생성을 간단하게 할 수 있게 되었습니다.

Stream의 Transform에 대해서, transform이나 flush를 얻을 수 있고, 이것을 사용하면 일부러 상속하지 않아도 Transform Stream을 만드는 것이 가능합니다. 아래와 같이 변경되었습니다.

before

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript
var Transform = require('stream').Transform;
var util = require('util');

util.inherits(MyTransform, Transform);

function MyTransform(opts){
Transform.call(this, opts);
}

MyTransform.prototype._transform = function(chunk, encoding, callback){
// 여기에서 변환하고
...
// push 합니다.
this.push(chunk);
};

MyTransform.prototype._flush = function(done){
// 마지막에 무언가 하고 싶다면 여기서 flush 합니다.
}

after

1
2
3
4
5
6
7
8
9
10
11
var transform = new stream.Transform({
transform: function(chunk, encoding, next) {
// 여기에서 변환하고
...
// push 합니다.
this.push(chunk);
},
flush: function(done) {
// 마지막에 무언가 하고 싶다면 여기서 flush 합니다.
}
});

여기서는 Transform Stream만 설명했지만, Readable이나 Writable도 간단히 생성할 수 있게 되었습니다.

자세한 내용은 이 링크를 참조하세요. https://iojs.org/api/stream.html#stream_simplified_constructor_api

LTTNG 서포트 (v1.2.0~)

HTTP의 Request 혹은 서버의 Response, GC가 실행될 때 커널레벨에서 어떤 일이 벌어지는지를 추적하기 위한 기능이 추가되었습니다. 지금까지의 Node.js에서는 이것을 실행하기 위해서 Dtrace를 사용해 왔습니다만, Linux의 Dtrace 구현은 미숙해서, 모두 구현되어 있는 것이 아니었습니다. 결과적으로, Joyent가 제공하고 있는 SmartOS 같은 Solaris를 기반으로 한 Unix 환경에서만 Node.js가 Dtrace를 사용할 수 있도록 지원하고 있었습니다.

이제는 Linux 커널에서도 표준으로 트레이스가 가능한 LTTNG도 사용할 수 있게 되었습니다.

자세한 설명은 아래 링크를 보세요.

Node LTTNG

Promise의 unhandleRejection / rejectionHandled 이벤트 (v1.4.1~)

Promise에서 catch를 빠트렸을 때 포착되지 않는 예외를 나타내는 unhandleRejection, rejected로 인해 더이상 catch에 도달하지 않는 예외를 검출하기 위한 rejectionHandled 이벤트가 새롭게 추가되었습니다.

여기 잘 설명된 azu 님의 예제를 발췌하도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
var bluebird = require("bluebird");
// unhandledRejection catch되지 않은 예외
process.on("unhandledRejection", function (reason, promise) {
console.log("unhandledRejection");
});

var resolved = bluebird.resolve();
resolved.then(function () {
throw new Error("Yay!");
});
1
2
3
4
5
6
7
8
9
10
var Promise = require("bluebird");
process.on("rejectionHandled", function (promise) {
console.log("rejectionHandled");
});
var rejected = Promise.reject(new Error("Error Promise"));
setTimeout(function () {
rejected.catch(function () {
// rejected된 promise에 'catch' 실행
});
},100);

Promise Error Handling

Buffer에 indexOf 메서드가 추가되었습니다. (v1.5.0 ~)

Buffer에 대해 indexOf 메서드를 사용할 수 있게 되었습니다. 지금까지는 일단 String으로 변환하는 등의 방법을 통해, 내용의 문자열을 검색할 필요가 있었지만, 그럴 필요가 없게 되었습니다.

before

1
2
3
4
5
6
7
var fs = require('fs');

// ./foo.txt => yosuke furukawa
fs.readFile(__dirname + '/foo.txt', function(err, buf){
var str = buf.toString();
console.log(str.indexOf('furukawa')); // 7
});

after

1
2
3
4
5
6
var fs = require('fs');

// ./foo.txt => yosuke furukawa
fs.readFile(__dirname + '/foo.txt', function(err, buf){
console.log(buf.indexOf('furukawa')); // 7
});

자세한 설명은 여기를 참조하세요.

https://iojs.org/api/buffer.html#buffer_buf_indexof_value_byteoffset

node의 커맨드에 –require 옵션으로 preload 모듈을 넘길 수 있게 되었습니다. (v1.6.0~)

1
$ node --require ./a.js b.js

사전에 preload해놓고 싶은 경우 사용할 수 있습니다.

process.nextTick에 여러 개의 매개변수를 넘길 수 있게 되었습니다. (v1.8.0~)

1
2
3
4
5
6
var obj = {};

process.nextTick(function(a, b) {
assert.equal(a, 42);
assert.equal(b, obj);
}, 42, obj);

이 변경으로 인해, process.nextTick의 callback에 임의의 매개변수를 전달할 수 있게 되었습니다. API가 setTimeout과 setInterval과 비슷하게 되었습니다.

REPL에 history 저장기능이 추가 (v2.0~)

REPL의 magic mode라고 불리는 기능이 추가되어 있습니다. 이는 환경변수 NODE_REPL_HISTORY_FILE을 지정하는 것으로 REPL을 종료하더라도 다음 실행 시에 자신이 실행한 REPL의 내용을 기억해주는 기능입니다. 이른바 REPL의 history 저장 기능입니다.

1
2
3
4
5
6
7
8
9
$ NODE_REPL_HISTORY_FILE=~/.node_history iojs
> var fs = require('fs');
> fs.readFile;
# Ctrl-D


$ NODE_REPL_HISTORY_FILE=~/.node_history iojs
> # push up button
> fs.readFile;

이는 NODE_REPL_HISTORY_FILE을 지정하지 않으면 절대 적용되지 않습니다. 저는 .zshrc에 다음과 같이 설정하는 것으로 대응하고 있습니다.

1
2
# iojs
alias iojs-repl="NODE_REPL_HISTORY_SIZE=Infinity NODE_REPL_HISTORY_FILE=~/.node_history iojs"

이렇게 하면 iojs-repl로 실행할 경우에는 히스토리가 적용됩니다.

자세한 설명은 여기를 참조하세요. https://iojs.org/api/repl.html#repl_repl

ES6에서 기본적으로 사용할 수 있는 구문이 추가(v2.0~)

제가 좀 전에 만든 iojs의 새 기능 소개 저장소에도 설명했지만, 일본어로 설명을 추가합니다.

class

class가 기본적으로 사용할 수 있게 되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// strict mode needed
'use strict';

class Animal {
constructor(name) {
this.name = name
}

say() {
// unimplemented
}
}

class Cat extends Animal {
say() {
console.log(`${this.name} < meow`);
}
}

var cat = new Cat('Mike');
cat.say(); // Mike < meow

이는 상당히 중요한데, io.js v2.0을 사용하는 데에는 util.inherits와 같은 상속의 헬퍼 메서드가 불필요하게 되고, 빌트인 클래스를 상속할 때도 사용 가능합니다.

enhanced object literals (v2.0~)

Object 리터럴 확장을 추가했습니다. 아래와 같이 key와 value의 변수명이 같을 때에는 간단히 작성할 수 있게 되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use strict';
// class
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
getInfo() {
let name = this.name;
let age = this.age;
let nextAge = this.age + 1;
// enhanced object literal
return {
name,
age,
nextAge
};
}
}

var bob = new Person('bob', 15);

console.log(bob.getInfo()); // { name: 'bob', age: 15, nextAge: 16 }

Rest 매개변수가 –harmony-rest-parameters 옵션을 통해 사용할 수 있게 되었습니다.

harmony option에 Rest 매개변수가 추가되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Rest parameters
function max(...args) {
// rest parameter is not Array-like object, that is just array.
console.log(Array.isArray(args)) // true
console.log(args.length) // 6

var max = args.reduce(function(max, n) {
return n > max ? n : max;
});
return max;
}

var maxNum = max(5, 15, 10, 1, 4, 5);
console.log(maxNum); // 15

실행하기 위해서는 아래와 같이 합니다.

1
$ iojs --harmony-rest-parameters es6/rest_params/rest.js

Computed property names가 –harmony-computed-property-names 옵션을 통해 사용할 수 있게 되었습니다.

harmony 옵션에 Computed property names를 사용할 수 있게 되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var i = 0;
var a = {
["foo" + ++i]: i,
["foo" + ++i]: i,
["foo" + ++i]: i
};

console.log(a.foo1); // 1
console.log(a.foo2); // 2
console.log(a.foo3); // 3

var param = 'size';
var config = {
[param]: 12,
["mobile" + param.charAt(0).toUpperCase() + param.slice(1)]: 4
};

console.log(config);

아래와 같이 실행합니다.

1
$ iojs --harmony-computed-property-names es6/computed_property/computedProps.js

Strong mode를 지원합니다. (v2.0~)

현재, Google의 V8팀은 여러가지 시도를 하고 있는데, 그 중 StrongScript라 불리는 시도가 있습니다. 이것은, JavaScript의 자유도가 높은 구문(var, arguments, \==, delete, for-in 등)을 가능한 deprecated로 처리해서, ES6을 포함한 최신의 구문(let, ...args, ===, Map, for-of)으로 바꾸기 위한 시도입니다.

이 StrongScript는 코드의 선두에, ‘use strong’ 디렉티브를 추가하는 것으로 Strong Mode가 되어, 실행됩니다. 마치 ‘use strict’를 통해 Strict Mode로 설정하는 것과 동일합니다.

이번의 io.js v2.0부터 Strong Mode가 –strong_mode 옵션 추가로 사용할 수 있게 되었습니다.

  • 참고로 아직 실험적 시도이기 때문에, 실제 환경에서 사용하는 것은 권장하지 않습니다.

var ==> let/const

1
2
3
'use strong';

var a = 'hoge';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ iojs --strong_mode strong_mode/vars.js

/Users/yosuke/iojs_v2_features/strong_mode/vars.js:3
var a = 'hoge';
^^^
SyntaxError: Please don't use 'var' in strong mode, use 'let' or 'const' instead
at exports.runInThisContext (vm.js:53:16)
at Module._compile (module.js:411:25)
at Object.Module._extensions..js (module.js:446:10)
at Module.load (module.js:353:32)
at Function.Module._load (module.js:308:12)
at Function.Module.runMain (module.js:469:10)
at startup (node.js:124:18)
at node.js:882:3

arguments => ...args

1
2
3
4
5
6
7
'use strong';

function some() {
let args = Array.prototype.slice.call(arguments);
}

some();
1
2
3
4
5
6
7
8
9
10
11
12
13
$ iojs --strong_mode strong_mode/arguments.js
/Users/yosuke/iojs_v2_features/strong_mode/arguments.js:4
let args = Array.prototype.slice.call(arguments);
^^^^^^^^^
SyntaxError: Please don't use 'arguments' in strong mode, use '...args' instead
at exports.runInThisContext (vm.js:53:16)
at Module._compile (module.js:411:25)
at Object.Module._extensions..js (module.js:446:10)
at Module.load (module.js:353:32)
at Function.Module._load (module.js:308:12)
at Function.Module.runMain (module.js:469:10)
at startup (node.js:124:18)
at node.js:882:3

eqeq => eqeqeq

1
2
3
4
'use strong';

if ('a' == 'a') {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ iojs --strong_mode strong_mode/eqeq.js

/Users/yosuke/iojs_v2_features/strong_mode/eqeq.js:3
if ('a' == 'a') {
^^
SyntaxError: Please don't use '==' or '!=' in strong mode, use '===' or '!==' instead
at exports.runInThisContext (vm.js:53:16)
at Module._compile (module.js:411:25)
at Object.Module._extensions..js (module.js:446:10)
at Module.load (module.js:353:32)
at Function.Module._load (module.js:308:12)
at Function.Module.runMain (module.js:469:10)
at startup (node.js:124:18)
at node.js:882:3

property를 삭제할 때, delete를 사용하지 않고, Map/Set을 사용

1
2
3
4
'use strong';
let obj = { key: 'value'};
delete obj.key;
console.log(obj);
1
2
3
4
5
6
7
8
9
10
11
12
13
$ iojs --strong_mode strong_mode/delete.js
/Users/yosuke/iojs_v2_features/strong_mode/delete.js:5
delete obj.key;
^^^
SyntaxError: Please don't use 'delete' in strong mode, use maps or sets instead
at exports.runInThisContext (vm.js:53:16)
at Module._compile (module.js:411:25)
at Object.Module._extensions..js (module.js:446:10)
at Module.load (module.js:353:32)
at Function.Module._load (module.js:308:12)
at Function.Module.runMain (module.js:469:10)
at startup (node.js:124:18)
at node.js:882:3

for-in => for-of

1
2
3
4
5
'use strong';

for (let k in [1, 2, 3]) {
console.log(k);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
$ iojs --strong_mode strong_mode/for.js
/Users/yosuke/iojs_v2_features/strong_mode/for.js:3
for (let k in [1, 2, 3]) {
^^
SyntaxError: Please don't use 'for'-'in' loops in strong mode, use 'for'-'of' instead
at exports.runInThisContext (vm.js:53:16)
at Module._compile (module.js:411:25)
at Object.Module._extensions..js (module.js:446:10)
at Module.load (module.js:353:32)
at Function.Module._load (module.js:308:12)
at Function.Module.runMain (module.js:469:10)
at startup (node.js:124:18)
at node.js:882:3

폭넓은 환경의 지원(v1.4.0, v1.6.0, v1.8.0)

이번에, 상당히 빌드 환경에 기합이 들어가 있어, 폭넓은 환경에서 바이너리가 제공되고 있습니다. Node.js였을 때는, darwin(osx), linux, sunos(solaris), windows의 바이너리가 제공되는 것뿐이었는데(그것만으로도 상당히 대단하긴 했지만), io.js부터는 ARM v6, v7을 포함해 상당히 많은 환경에서 바이너리가 제공되고 있습니다.

binary는 제공되지 않지만, Android에서 기동하기 위한 옵션이 있거나, SmartOS, FreeBSD에서 기동하기 위한 옵션이 제공되고 있습니다.

또 여러 OS의 가상 환경을 구축하고 그 위에서 테스트를 실행하고 있습니다. 어딘가의 환경에서 에러가 발생하면 즉시 알 수 있게 되어있습니다.

https://jenkins-iojs.nodesource.com/job/iojs+any-pr+multi/

의존 라이브러리 관련(v2.0.0~)

v8이 4.2가 되었습니다.

v8이 새로워졌기 때문에, JavaScript의 최적화 및 ES6의 기능이 늘어났습니다. 이에 관해서는 좀 전에 소개했습니다. 또 v8이 업그레이드되면서, V8의 API가 갱신되고 Native Module도 변경이 필요하게 됐습니다.

즉, 낡은 Native module의 상태로는 작동하지 않게 되었습니다.

현 시점에서 v2.0에서 동작하지 않는 모듈 목록에 관해서는 아래의 이슈를 읽어 주세요.

github.com

nan갱신하는 것만으로 가능한 모듈도 많기 때문에, 잊고 있는 모듈에 관해서는, 지금 nan을 갱신해서 풀 리퀘스트를 통해 알려주는 것이 좋지 않을까 하고 생각합니다. 다만 이번 v8이 갱신되는 것 만으로, 동작하지 않게 되는 모듈이 많으면 곤란하기 때문에, nan을 core에 넣자는 제안도 있습니다.

OpenSSL v1.0.2a가 되었습니다.

OpenSSL이 v1.0.2a가 되고, crypto의 성능이 개선되었습니다. 이 글이 자세히 설명하고 있습니다. [^1]

github.com

http-parser v2.5.0이 되었습니다.

http-parser가 드디어 갱신되었습니다. 일단 http-parser는 v2.4가 된 후, 버그가 발견되고 버전이 돌아갔지만, 그 버그가 수정되어, 고속화된 http-parser가 사용할 수 있게 되었습니다.

libuv가 v1.4.2가 되었습니다.

libuv가 새로워졌습니다. windows/linux에서의 버그 수정 및 ARM에서도 동작하기 위한 수정 등이 포함되어 있습니다.

npm이 v2.9.0이 되었습니다.

npm도 새로워졌습니다. 번역이 있으므로, 한 번 보는 것도 좋겠습니다. medium.com

정리 (v1.0 ~ v2.0의 다른 점)

  • Stream을 간단히 만들 수 있게 되었습니다.
  • LTTNG의 지원
  • promise의 unhandledRejection / rejectionHandled 이벤트
  • Buffer의 indexOf 메서드 추가
  • node –require 옵션 추가
  • repl에 history 옵션 추가
  • v8이 새로워지고, class, object 리터럴의 확장 etc와 Strong Mode의 추가
  • ARM을 시작으로, 바이너리 제공과 폭넓은 환경의 지원
  • 의존 라이브러리의 갱신을 통한 성능 향상, 버그 수정 등

끝맺음

자, 이번에 v1.0부터 지금까지의 변경 사항을 슥삭 정리해 봤습니다. 앞으로의 변경사항은 일본어로 작성되어, blog.iojs.jp에 정리되고 있습니다.[^2]

변화가 극심한 io.js를 따라가는 것은 힘든 일이지만, 이 글을 매주 조금씩 읽어둔다면 도움이 될 것입니다.

또, 코어 팀은 지금 v3.0을 향해서 움직이기 시작했습니다. 저도 조금씩 기여하고 있습니다. 이 변화를 즐기면서 공헌할 수 있다면 좋겠다고 생각합니다.

[^1]: 大津 님이 써주셨습니다. 大津 님++

[^2]: 언제나 번역해주시는 @watilde 님, @jgs 님, @kosamari 님에게 감사드립니다.