이벤트 루프는 6개의 페이즈로 구성되어 있다.
setTimeout은 timer phase
setImmediate는 check phase
각 페이즈를 돌면서 실행 가능한 콜백을 발견하면 순차적으로 실행을 한다.
setTimeout(()=>{
console.log("test1");
}, 0);
setImmediate(()=>{
console.log("test2");
});
여기서 위 코드를 실행 해보면
출력 결과가
"test1"
"test2"
또는
"test2"
"test1"
이 반복되는 것을 볼 수 있다.
싱글 스레드는 순서를 보장해주는 것이 아닌가?
위 코드가 실행이 될 때
각 페이즈가 갖고 있는 큐에 들어 간다.
그리고 이벤트 루프가 각 큐를 방문 할 때 실행할 준비가 되면 실행이 된다.
그런데 맨 처음 실행되는 timer phase에 있는 setTimeout은 실질적으로는 1ms 후에 실행이 된다.
V8엔진, NodeJS에서는 최소 1ms 후 실행되게 설계되어 있음.
방문
이 발생 할 때 시점이 1ms 전이냐 후이냐에 따라 실행 결과가 달라진 것이다.
즉 방문
이 1ms 전에 발생하는 경우 setTimeout은 준비가 되지 않았기 때문에
"test2"
가 먼저 실행이 되고
만약 방문
이 1ms 후에 발생하는 경우 setTimeout이 준비가 되었기 때문에 "test1"
이 먼저 실행 되었던 것이다.
이러한 이벤트 루프의 방문
또는 큐에 진입속도
는 cpu에 상태에 따라 조금 씩 달라질 수 있다.
\
const fn = () => {
process.nextTick(fn)
}
setTimeout(()=> {
console.log("Never reachead");
}, 0)
fn()
위와 같은 형태의 코드인 경우 setTimeout는 영원히 실행하지 못한다.
그 이전의 방식에서 브라우저의 방식과 통일하기 위해 현재의 형태로 바뀌었다.
net 패키지의 server.listen() 내부 구현 코드를 보면
nextTick을 사용하여 'listening' 이벤트를 emit 한다
setImmediate를 사용해도 될텐데 왜 그럴까?
setImmediate를 사용하였을 때
만약 'listening' 이벤트가 발생하기 전에 다른 이벤트가 먼저 check phase의 큐에 들어가게 된다면
'listening'의 콜백의 실행 시점이 약간 늦어 질 수 있어,
서버의 상태를 확인하려고 하는 코드가 정상 작동하지 않을 수 있다.
그래서 nextTick
을 사용하여 check의 큐를 진입하기 전에 'listening'을 실행 시키는 것 이다.
const net = require("net")
const server = net.createServer();
server.listen(8080, ()=>{
console.log("서버 오픈")
});
setTimeout(()=>{
console.log("setTimeout")
}, 0)
process.nextTick(()=>{
console.log("before listen 1")
})
server.on('listening', () => {
// listen 코드가 먼저 실행된 이유는
// 콜백 함수가 nextTick으로 실행되기 때문이다.
console.log("listen")
});
process.nextTick(()=>{
console.log("before listen 2")
})
// 출력 순서
// 서버오픈
// listen
// before listen 1
// before listen 2
// setTimeout
앞의 before listen 1
보다 listen
이 먼저 출력되는 이유는
on의 등록은 동기적으로 되고
server.listen이 poll 페이즈에서 실행이 될 때 on의 콜백이 먼저 nextTick 큐에 진입했기 때문이다.
net : https://github.com/nodejs/node/blob/61b4d60c5d/lib/net.js#L1407
그 동안 막연하게 알고 있던 NodeJS의 이벤트 루프에 대해 다시 알아보았다.
지금 이 글을 작성하는 시점 이전에도 아래의 링크들을 참고해서 읽어 본적이 있었지만
이해를 하지 못하고 넘어 갔었다.
최근 회사에서 Go를 통한 작업을 하게 되면서
여러 자료구조들을 접하게 되었는데
그에 대한 영향인지 잘은 모르겠지만 갑자기 이해가 쏙쏙 되는 것 같은 느낌이다.
NodeJS 개발자로서 이벤트 루프의 내부 동작원리를 이해한다면
코드를 작성함에 있어 큰 도움이 있을 거라 생각이 든다.
이 글은 나만의 정리 방식이니 아래의 링크들을 꼭 참고 하였으면 한다.