이번 노트에서는 Node.js에서 이루어지는 비동기 실행의 원리에 대해서 조금 더 깊게 배워보겠습니다. 혹시 이전 노트에 배웠던 setTimeout 함수 기억나시나요? setTimeout 함수는 특정 밀리세컨즈 초 후에 특정 코드를 실행하도록 설정하는 함수였는데요. 예를 들어,
setTimeout(() => {
console.log('test');
}, 1000);
이 코드를 실행하면 1000밀리세컨즈 초(1초) 후에 test라는 단어가 화면에 출력됩니다.
또 다른 예시 코드를 보면,
let num = 1; // 1번
setTimeout(() => { // 2번
num = 2; // 5번
}, 1000);
num = 3; // 3번
console.log(num); // 4번
이 코드에서는 마지막에 num 변수의 값으로 3이 출력됩니다. 왜냐하면 지금 주석에 적힌 순서대로 코드가 실행되기 때문입니다.
자, 그런데 여기서 한 가지 엉뚱한 문제를 내보겠습니다.
만약 제가 setTimeout의 두 번째 인자로 1000이 아닌 0을 주면 어떻게 될까요? 이렇게 말입니다.
let num = 1;
setTimeout(() => {
num = 2;
}, 0); // 1000 -> 0 밀리세컨즈로 수정
num = 3;
console.log(num);
이렇게 setTimeout 함수의 두 번째 인자로 0 밀리세컨즈를 주었습니다. 0초 후에 콜백을 실행하라는 뜻인데요.
자, 이 경우에 출력되는 num 변수의 값은 무엇일까요? 2가 출력될까요? 3이 출력될까요? 답은 3입니다.
사실, 이 문제의 답은 이때까지 배운 내용만 가지고는 여러분이 해결하기 힘든 문제입니다. 왜냐하면 Node.js에서 비동기 실행이 구체적으로 어떻게 동작하는지 그 깊은 원리까지 알아야 답을 구할 수 있는 문제이기 때문입니다. 잠깐 아래 이미지를 볼까요?
지금 왼쪽 흰색 바탕에 있는 자바스크립트 코드를 node로 실행한다고 해봅시다. 그럼 다음과 같은 일이 벌어집니다.
node는
하나의 스레드로 자바스크립트 코드를 차례대로 실행하고,
그 스레드는 그 후에 바로 Event Loop라는 걸 돌게 됩니다.
Event Loop란 각종 콜백들의 실행 조건(특정 시간이 경과했는지 등)을 확인하고, 실제로 콜백을 실행하는 로직을 말하는데요. Event Loop는 특정 콜백의 실행 조건이 만족된 것을 확인하면 Queue라는 곳에 콜백(callback)들을 삽입합니다. Queue라는 것은 자료 구조에서 배우는 큐(queue)처럼 구현된 저장소입니다. 여기서는 콜백들이 저장되는 저장소죠.
이 경우에는 1초가 지났다면, 하늘색 표시된 콜백이 Queue에 담기게 됩니다. 이렇게 말이죠.
그리고 Queue에 담긴 콜백을 Event Loop가 실행합니다.
콜백이 한 개 이상인 경우도 볼까요?
이렇게 콜백이 여러 개일 때는 각 콜백의 실행 조건이 충족될 때, 차례대로 Queue에 들어가게 되고,
콜백들은 Queue에 들어간 순서대로 Event Loop에 의해 실행됩니다.
자, 이제 아까 그 문제에 대한 답을 해보도록 합시다.
let num = 1;
setTimeout(() => {
num = 2;
}, 0); // 1000 -> 0 밀리세컨즈로 수정
num = 3;
console.log(num);
왜 이 코드에서는 변수 num의 값으로 3이 출력된 걸까요? 방금 배운 콜백의 실행 원리를 생각해보면 당연한 겁니다.
일단 node는 자바스크립트 코드를 하나의 스레드로 실행합니다.
이때 중요한 것은 지금 setTimeout에 있는 콜백을 실행해도 되는지(0초가 지났는지)에 대한 판단을 하는 것은 아니라는 점입니다. 이 순간에는 단지 콜백을 등록만 해둘 뿐입니다. 그리고나서 연이어 3번 코드, 4번 코드를 실행하기 때문에 num 변수의 값으로 3이 출력된 건데요.
자바스크립트 코드를 다 실행한 스레드는 이제 Event Loop를 돌면서
콜백 중에 Queue에 담아야할 것들은 없는지 확인합니다. 현재 하늘색 콜백은 '0초 후 실행되어야 한다는 조건'을 만족하면 되니까 당연히 바로 Queue에 담습니다.
그리고 이렇게 큐에 담긴 콜백(callback)을 실행하는 겁니다. 이때서야 num 변수에 2가 대입되는 거죠.
그런데 이렇게 되면 제가 setTimeout 함수에 인자로 준 0이라는 시간보다 한참 후에야(물론 컴퓨터에서는 아주 짧은 시간이겠지만요) 콜백이 실행되는 거 같은데 억울하지 않을까요? 하지만 이건 Node.js의 비동기 실행 구조상 어쩔 수 없는 부분입니다.
이것과 비슷한 예로, setTimeout 함수에 300 밀리세컨즈 초를 두 번째 인자로 주었다고 가정했을 때,
만약 Queue에 실행해야할 콜백들이 이미 앞에 밀려있다면 300 밀리세컨즈 초보다 늦게 실행되는 경우도 있습니다. 한 가지 확신할 수 있는 것은 300 밀리세컨즈 초가 지나기 전에 더 일찍 실행되는 경우는 없다는 점뿐이죠.
자, 이때까지의 내용을 간단히 정리해보면 Node로 자바스크립트 파일을 실행하면 다음과 같은 일들이 수행됩니다.
1단계 : 하나의 스레드가 자바스크립트 코드를 죽 실행하고
2단계 : 그 후에, 그 스레드가 Event Loop라는 로직에서 각각의 콜백들에 대한 실행 여부를 판단하고, Queue에 넣은 후, Queue에 담긴 콜백들을 실행한다.
그리고 바로 이 스레드가 제가 '비동기 프로그래밍의 장점과 Node.js의 내부(심화)' 노트에서 말했던 Node.js에서 '빠르게 끝나는 일을 담당하는 메인 스레드'입니다. 해당 노트와 연결지어서 이해하시면 좋을 것 같네요.
자, 그럼 여러분이 내용을 제대로 이해했는지 응용 문제를 통해 테스트해봅시다.
아래 코드에서 'test'라는 단어는 출력이 될까요?
console.log('Start!');
setTimeout(() => {
console.log('test');
}, 0); // 0 밀리세컨즈
while (true) { // 의미없는 while 문이 자바스크립트 실행의 종료를 막고 있음
}
답은 '절대 출력되지 않는다'입니다. 이 코드를 실행해보면
test라는 단어는 절대 출력되지 않는 것을 알 수 있습니다.
그 이유는 맨 마지막에 있는 무한 반복되는 while 문 때문인데요. 일단 자바스크립트의 실행 자체가, 무한 반복되는 while 문 때문에 종료될 수가 없어서, EventLoop에서 콜백을 다루는 그다음 단계로 아예 넘어가지 못하기 때문에 그렇습니다.
이런 것은 setTimeout 함수뿐만 아니라 다른 비동기 함수들도 마찬가지입니다. 예를 들어 아래와 같이 readFile 함수를 쓴 코드를 실행한다고 해봅시다.
const fs = require('fs');
console.log('Start!');
fs.readFile('./new', 'utf8', (err, data) => {
console.log('test'); // 절대 실행되지 않음
});
while (true) { // 의미없는 while 문이 자바스크립트 실행의 종료를 막고 있음
}
마지막의 무한 반복되는 while 문으로 인해 자바스크립트 실행 자체는 종료되지 않습니다. 그럼 마찬가지로 콜백을 다루는 단계로 넘어가지 못하기 때문에 아무리 파일 내용을 다 읽었다고 해도 'test'라는 단어는 절대 출력될 수 없는 거죠.
Node.js에서 비동기 함수의 콜백이 어떤 식으로 실행되는지 이제 어느 정도 이해되시죠? 그런데 아쉽게도 제가 설명드린 내용은 설명을 위해 실제보다 정말 간소화한 내용입니다. 실제 Node.js의 내부 구현을 보면,
이런 식으로 여러 종류의 Queue들이 있고, 각 콜백들은 그것을 등록한 함수에 따라 서로 다른 Queue에 담기게 됩니다. 그리고 Event Loop가 각 Queue의 콜백을 판단하고 처리하는 방식에도 조금씩 차이가 있는데요. 이 부분까지 들어가게 되면 너무 내용이 어려워지기 때문에 일단은 이 정도에서 멈추겠습니다. 혹시 이 부분에 대해 더 깊게 공부하고 싶은 분들은 'Node.js EventLoop'라고 인터넷에 검색해보시거나, Node.js 공식 문서에 있는 이 문서와 이 문서를 참조하세요.
이번 노트에서는 Node.js에서 비동기 함수의 콜백이 어떤 식으로 실행되는지 그 내부 원리를 공부해봤습니다. 이번 노트의 내용만 알아도 당장 Node.js 개발을 하는 데 큰 문제는 없겠지만, Node.js 개발을 계속하다 보면, Node.js에서 이루어지는 비동기 실행에 대해 좀더 깊게 공부해야 할 때가 올 겁니다. 혹시 관심이 있는 분이라면 별도로 더 깊게 공부해보시는 것을 추천합니다.
*참고로 공식 문서의 내용에 따르면 setTimeout 함수에서 시간(delay) 인자로 2147483647보다 큰 값 또는 1보다 작은 값을 주면, 시간(delay) 인자에 실제로는 1이 설정됩니다.