2011년 6월 22일 수요일

[node.js] 루프와 process.nextTick()

node.js 암초 2번째 이야기. 이번에는 node.js의 루프와 process.nextTick() 관한 이야기를 적어볼까 한다. (루프가 뭔지는 굳이 설명할 필요는 없을 것 같다.)

for (var i=0; i < 10; i++) {
    console.log('work', i);
}
전형적인 for 루프 예제이다. 이 코드의 의도는 work 0 에서 work 9 까지 10개의 문자열이 찍는 것이고, 의도대로 동작한다.

그런데 이런 단순한 루프의 경우 node.js 로 구축한 서버에서에 한해서 상당히 곤란한 점이 있다. 멀티쓰레드 구조가 아니기 대문에 루프가 길어질 경우 이 루프의 처리가 끝날 때 까지 서버가 다른 이벤트를 받아들이지 못 하는 상황이 발생하게 될 지도 모른다는 것이다. 서버 입장으로써 굉장히 치명적이다.

그래서 이런 루프에서 처리할 내용을 이벤트큐에 쌓아서 코드 처리를 분산해서 처리할 수 있도록 도와주는 것이 process.nextTick() 이다.

process.nextTick(callback) 이라는 형식으로 사용되며 의미상으로는 setTimeout(callback, 0) 와 동일하다. (공식 API 문서에서는 setTimeout에 비해 뭔가 효율적이라는데 정확히 뭐가 다른지는 설명해 주질 않는다 -_-)

그래서 소스를 다음과 같이 수정할 수 있다.
for (var i=0; i < 10; i++) {
    process.nextTick(function () {
        console.log('work', i);
    });
}
그런데 이 코드는 큰 문제가 있다. 실행시켜보면 'work 9'만 10번 찍힌다. 전혀 의도한 대로 동작하지 않는다.

문제는 nextTick에 등록된 callback이 비동기로 동작하기 때문에다. 루프가 다 끝난 후에 이 콜백이 이벤트큐에서 빠져나오면서 하나하나 실행되기 시작하는데, 그 시점이 i 가 9가 되어서 for루프를 빠져나온 시점이기 때문이다.

그렇다면 함수의 인자로 넘겨줘볼까?
for (var i=0; i < 10; i++) {
    process.nextTick((function (v) {
        console.log('work', v);
    }) (i));
}
이 코드는 제대로 실행되지 않는다. 일단 9 까지 순서대로 찍히긴 찍히지만 다음과 같은 식의 에러가 발생한다.
node.js:nnn
        throw e; // process.nextTick error, or 'error' event on first tick
        ^
TypeError: Property '0' of object ,,,,,,,, is not a function
    at EventEmitter._tickCallback (node.js:nnn:nn)
솔직히 무슨 내용인지 알아먹을 수가 없다. 일단 변수 스코프 문제를 연관지어서 생각해봐야 할 것 같다. 즉 i가 사라진 시점에 이 i 자체를 인자로 넘겨주려고 하다보니 뭔가 문제가 생긴 것이 아닐지...

그렇다면 스코프를 살려주도록 고쳐보자.
for (var i=0; i < 10; i++) {
    (function (dupi) {
        var value = dupi;
        process.nextTick(function () {
            console.log('work', value);
        });
    })(i);
}
뭔가 코드가 줄줄 늘어나버렸다. 굉장히 보기 싫어진 것 같다.

특징적으로 웹앱의 자바스크립트에서 많이들 활용하는 'function 을 활용한 코드 블럭 만들기' 기법을 사용했다. (function (args...) { ... })(params...) 식으로 함수를 정의함과 동시에 호출하는 것이기 때문에 동적인(?) 코드블럭으로 생각하면 되겠다.

for 루프 내부에서 바로 nextTick을 호출하지 않고 이 코드 블럭을 호출한다. 이 블럭 내에서는 i의 값을 로컬변수 value에 저장하고 nextTick을 호출해서 코드블럭의 스코프에 해당하는 value 라는 값을 바로 인쇄하도록 한다.

실행 결과는 가장 처음의 예제코드와 동일하게 나온다. 즉, 의도대로 동작하는 것이다.

비동기 코딩스타일에서는 역시 익숙한대로 코딩하다가는 큰코 다칠만한 주제가 많은 것 같다. 생각하는 방식을 바꿔야 할 지도 모른다. 위와 같은 식의 문제를 해결하려고 했다가 가독성이 뚝뚝 떨어지는 코드를 생산(?)해야 할 지도 모르기 때문에 좀 안타깝다.

댓글 3개 :

오디스 :

node에 대해 정리하다가 아티클을 보게 되었네요.
중간에 오류는 process.nextTick의 인자로 함수를 넣어주지 않아서 발생한 것입니다. 다음과 같이 수정되어야 합니다.
for(var i = 0; i < 10; i++) {
process.nextTick((function(num) {
return function() {
console.log('work', num);
};
})(i));
};

감사합니다.~

Seorenn :

오디스// 그 방법을 생각 못했네요. 감사합니다. :)

몽상가 :

setTimeout 보다 효율 적이라고 한건,

이벤트 루프 그러니깐 nodejs 의 내부에서 이벤트 루프가 끝날 때 큐에 걸려있는 모든 nextTick callback 을 일괄 처리해줘서 그런거 아닐 까요?

setTimeout(cb, 0) 구문은 이벤트 루프에서 처리되는 것보다 부자연 스러운 그러니깐 부차적인 이펙트를 만들기 때문에 그것보다 효율적이라고 말하는 것 같네요.. ㅎㅎ