몇몇 사람들이 process.nextTick()을 혼동하는 것을 보았습니다. process.nextTick()이 무엇을 하고 언제 사용하는지 살펴보겠습니다.
이미 알고 있겠지만 모든 노드 어플리케이션은 싱글스레드로 동작합니다. 이 말은 I/O와는 항상 분리되어 있고 노드의 이벤트 루프는 오직 하나의 작업과 이벤트만 처리한다는 의미입니다. 이벤트루프의 모든 Tick에서 Node가 처리하는 콜백을 큐에 추가하는 이벤트루프를 생각할 수 있습니다. 그래서 멀티코어상에서 노드를 운영하더라도 실제 처리에서 병렬구조(parallelism)의 어떤 이점도 얻을 수 없습니다. 모든 이벤트는 한번에 하나씩 처리된다. 그래서 노드는 I/O가 많은 작업에 적합하고 CPU 작업량이 많은 작업에는 적합하지 않습니다. 모든 I/O기반 작업에서 이벤트큐에 추가될 콜백을 쉽게 정의할 수 있습니다. 콜백은 I/O작업이 완료되면 실행되고 동시에 어플리케이션은 다른 I/O작업에 대한 요청을 계속해서 처리할 수 있습니다.
process.nextTick()은 액션의 실행을 이벤트루프의 다음 차례까지 실제로 연기합니다. 간단한 예제를 보겠습니다. 다음 Tick에서 호출해야 하는 foo()함수가 있다면 다음과 같이 사용합니다.
function foo() {
console.log('foo');
}
process.nextTick(foo);
console.log('bar');
위의 코드를 실행하면 bar가 foo보다 먼저 출력됩니다. 이는 foo()의 호출을 이벤트루프의 다음 Tick까지 연기했기 때문입니다.
bar
foo
이는 setTimeout()을 사용해도 같은 결과를 얻을 수 있습니다.
setTimeout(foo, 0);
console.log('bar');
하지만 process.nextTick()는 단순히 setTimeout(fn, 0)의 별칭이 아니고 더 효율적입니다.
어디서 process.nextTick()를 사용할 수 있는지 살펴보겠습니다.
CPU 부하가 심한 작업의 실행을 다른 이벤트로 나누어 처리하기
CPU 작업량이 많은 계산을 계속해서 실행해야 하는 compute() 작업을 가정해 보겠습니다. 같은 노드 프로세스에서 HTTP요청의 처리같은 다른 이벤트도 처리해야 한다면 process.nextTick()을 사용해서 다른 이벤트를 처리하면서 compute()의 실행을 나누어서 처리할 수 있습니다.
var http = require('http');
function compute() {
// 복잡한 계산을 계속해서 수행한다
// ...
process.nextTick(compute);
}
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
}).listen(5000, '127.0.0.1');
compute();
이 예제에서 compute()를 재귀적으로 호출하는 대신 process.nextTick()를 사용해서 이벤트루프의 다음 tick까지 compute()의 실행을 지연시켰습니다. 이렇게 함으로써 다른 HTTP요청이 이벤트루프에 큐에 들어온다면 다음 compute()가 호출되기 전에 처리될 것입니다. process.nextTick()를 사용하지 않고 그냥 compute()를 재귀적으로 호출했다면 프로그램은 들어오는 HTTP 요청을 처리할 수 없을 것입니다. 이부분은 직접 테스트해보세요.
안타깝게도 process.nextTick()를 사용해도 멀티코어 병렬구조의 이득을 전혀 얻지 못합니다. 하지만 process.nextTick()을 사용하면 어플리케이션의 다른 부분에서 CPU를 공유해서 사용할 수 있습니다.
콜백을 비동기로 유지하기
콜백을 받는 함수를 작성했을 때 이 콜백이 비동기로 실행됨을 항상 보장해야 합니다. 이 규칙을 위반하는 예제를 살펴보겠습니다.
function asyncFake(data, callback) {
if(data === 'foo') callback(true);
else callback(false);
}
asyncFake('bar', function(result) {
// 이 콜백은 실제로는 동기로 실행된다!
});
왜 이 방법이 잘못되었는지 Node 레퍼런스 문서에서 가져온 예제를 보겠습니다.
var client = net.connect(8124, function() {
console.log('client connected');
client.write('world!\r\n');
});
위 예제에서 어떤 이유에서든 net.connect()가 동기로 실행된다면 콜백은 즉시 실행됩니다. 그러므로 client 변수는 client에 내용을 작성하려고 콜백에서 접근했을 때 초기화되지 않았을 것입니다.
asyncFake()를 다음과 같이 항상 비동기로 실행되도록 고칠수 있습니다.
function asyncReal(data, callback) {
process.nextTick(function() {
callback(data === 'foo');
});
}
이벤트가 발생했을 때
소스를 읽어서 읽은 내용의 청크(chunk)를 담고 있는 이벤트를 발생시키는 라이브러리를 작성해 보겠습니다. 이 라이브러리는 다음과 같을 것입니다.
var EventEmitter = require('events').EventEmitter;
function StreamLibrary(resourceName) {
this.emit('start');
// 파일을 읽고 읽은 청크마다 다음을 수행한다.
this.emit('data', chunkRead);
}
StreamLibrary.prototype.__proto__ = EventEmitter.prototype; // EventEmitter를 상속받는다
다른 곳에서 이 이벤트를 리스닝하고 있다고 하겠습니다.
var stream = new StreamLibrary('fooResource');
stream.on('start', function() {
console.log('Reading has started');
});
stream.on('data', function(chunk) {
console.log('Received: ' + chunk);
});
위 예제에서 리스너는 생성사가 호출되면서 즉시 발생하는 StreamLibrary의 이벤트인 start 이벤트를 절대 얻지 못할 것입니다. 이벤트가 발생할 때는 start 이벤트에 아직 콜백이 할당되지 않았습니다. 그래서 이 이벤트를 받지 못합니다. 여기서도 process.nextTick()를 사용해서 리스너가 start 이벤트를 찾을 수 있도록 이벤트의 발생을 지연시킬 수 있습니다.
function StreamLibrary(resourceName) {
var self = this;
process.nextTick(function() {
self.emit('start');
});
// 파일을 읽고 읽은 청크마다 다음을 수행한다.
this.emit('data', chunkRead);
}
process.nextTick()에 대한 궁금증이 풀렸기를 바랍니다. 빠뜨린 부분이 있다면 댓글로 공유해 주길 바랍니다.
오우. 이글 번역하셨군요. 감사합니다. ^^
제가 판단하는 process.nextTick은 이래요.
이것을 비동기라고 하는데, 이 부분을 많은 개발자들이 오해할 수 있죠. 왜냐하면 ajax 를 너무 많이 접한 나머지, 내가 명령한 일을 처리후에 콜백을 받는다는 의미.. ㅋ.
nextTick은 완전 다른건데 말이죠.. 단지 nextTick은 function의 실행을 defer(연기)한다는 의미가 강하지 . 설정한 callback이 실행되어 나중에 결과가 이미 나온것과 같이 실행되는게 아니다라는것입니다.
확실히 nextTick이 setTimeout보다 먼저 tick안에서 실행되고 있습니다.
번역 감사합니다. ~~ KIN플
비동기라고 설명하는 부분에 말씀하신 것처럼 오해할 여지도 있겠군요. 비동기는 맞기에 그런 오해에 대해서는 생각해 보지 못했었네요. 이벤트루프에 대한 설명이 항상 함께 있어야 하겠군요.
setTimeout(fn, 0)을 사용했었는데 이젠 process.nextTick()을 사용해야겠네요. 유익한 내용입니다.
아주 최근 버전가지 확인해 보진 못했지만 nextTick과 setTimeout으로 할때 작업 큐에 들어가는 우선순위의 차이가 있습니다. 사용하시기 전에 이점 확인해 보셔야 합니다.
좋은내용 감사합니다. process.nextTick()이 더 잘 이해되었어요.
안녕하세요. 좋은 글 번역 감사합니다 :)
Node 공식문서의 이벤트루프 부분을 읽고, 몇가지 궁금한 점이 있습니다.
위의 코드에서 process.nextTick 으로 compute 함수를 재귀 실행시키면, http 요청이 들어왔을때 이를 지연없이 처리할 수 있다고 하셨는데, 제가 생각했을때는 nextTick 으로 compute 함수를 지연시키고 http 요청이 왔을때 그 콜백 내용을 이벤트루프 poll단계의 queue에 넣어주는 것 뿐이고, 어차피 process.nextTick은 단지 실행해야하는 내용을 현재의 작업 바로다음으로 지연시켜주는 것이라, 정확히 http요청의 콜백함수가 실행될때까지는 nextTick 을 사용하든 안하든 똑같은 지연이 존재하는 것 같아서요.
이부분 질문드립니다~!
아 이 원글은 시간 지난 시점에서는 좀 신경쓰이는 글이긴 합니다. 번역 중심이라 놔두고 있지만요.
저 글은 노드 0.6.8때인데 저때는 nextTick을 CPU을 완전히 차지하지 않도록 쓰는게 일반적인 팁이었다면 언젠가부터는 그렇게 하지 않습니다. 공식 문서를 읽고 오히려 이 글이 헷갈리게 만든것 같은데 공식문서에서 이해하신게 맞습니다. 이부분은 Node가 발전하면서 동작이 바뀐건지 제가 잘못알고 있었던 것인지 까지는 정확하지 않습니다.(몇번 찾아보려다가 못찾았네요.)
https://gist.github.com/brycebaril/ff86eeb90b53fd0c523e
이 글이 도움이 될것 같습니다. 말씀하신대로 nextTick이 큐의 처음에 바로 넣어주기 때문에 저 예제 코드에서 그냥 compute를 돌리는 것과 nextTick을 사용하는 것이 거의 같습니다.(혼란을 드려 죄송합니다.)