게시글을 읽으시기 전에,
제 블로그의 정보가 정확하지 않을 수도 있다는 점
명심해 주세요. 혹시 틀린 부분을 발견하신다면 댓글로 알려주시면
적절한 조치를 취하도록 하겠습니다.
서론
이번 포스트에서는 Node.js를 사용할때 알아두어야 할 중요한 개념인 EventLoop와 V8 JavaScript Engine의 동작구조에 대해 알아보려 한다. Node.js의 기본 특성을 모른다면 이해가 힘들 수 있으니 만약 모른다면 이전글을 읽어주고 오길 바란다. 또한 자료구조도 알고 있다면, 기본적인 구조를 이해할때 더 쉽게 다가올 수 있으나, 만약 모른다고 하더라도 이 글에서 기본적인 개념 설명은 해줄 것이기 때문에 걱정은 안 해도 된다. 자 그럼 본격적으로 EventLoop와 V8 JavaScript Engine의 구조에 대해 파고 들어보자.
EventLoop의 기본개념
EvenetLoop는 내가 전 게시글에서 설명했던바와 같이, javaScript를 처리하는 스레드이다. 또한, EventLoop는 시스탬 커널에 가능할때마다 작업을 떠넘겨 싱글 스레드임에도 불구하고 none-blocking I/O작업을 도와준다. 이같은 작업이 가능한 이유는 근래의 시스탬 커널은 대부분 멀티 스레드이기 때문에 백그라운드에서 실행되는 작업들을 관리할 수 있기 때문이며, 만약 어떠한 작업이 완료되었을시 커널은 Node.js에 알려 적절한 콜백함수를 poll 큐에 추가하여 실행할수 있게 만든다.
V8 JavaScript Engine의 구조
이제 본격적으로 V8 JavaScript Engine의 작동구조를 알아보자.
일단 단계별로 작동 순서를 나열해보겠다.
- 브라우저에서 요청한 javascript가 V8 javaScript engine(ex -> crome)으로 전달된다.
- V8 javaScript engine은 받은 javascript를 Call stack에 넣는다.
- 만약 받은 javascript함수에 비동기 함수가 포함되어 있다면 webAPI(node에선 백그라운드)로 보낸다.
- 비동기 작업이 완료되면 callback함수를 callback queue에 넣는다.
- callstack이 비면 EventLoop가 실행되며, callback queue에 있던 함수들이 callstack으로 들어간다.
- 실행중인 callstack에 비동기함수가 있다면 위 과정을 반복한다
- 최종적으로 모든 작업이 끝난다면 브라우저는 화면을 업데이트하여 사용자에게 보여준다.
자, 한번 살펴보았는데 아마 대부분의 사람들은 전체적인 흐름을 이해는 했지만 callback queue가 뭔지, callstack은 또 뭔지 이해 못했을꺼라고 생각한다. 그러므로 이제부터 자세하게 각 파트가 무슨일을 하는지 파고들어보자.
callStack
call stack은 실행될 함수들을 담은 stack이다. 여기서 stack이란 LIFO(Last In First Out) 구조를 가진 자료구조이다. 즉, 마지막으로 들어온 함수가 제일 처음 실행 된다는것이다. 또한, Node.js은 단일 스레드 구조기 때문에 단일 호출스택 구조이며, 이 말은 곧 한번에 한가지의 일을 처리 할 수 있다는 것이다. callStack은 다음에 무슨 함수를 처리할지 담고 있기 때문에, 다른 말로 하면 호출 스택은 프로그램에서 현재 어디에 위치하고있는지 나타낸다고 할 수 있다. 자, 그럼 예제를 통해서callStack의 동작과정을 순서대로 알아보자.
//곱하기 함수
function multiply (x, y) {
return x * y;
}
//제곱 출력 함수
function printSquare (x) {
const s = multiply(x, x);
console.log(s);
}
printSquare(5);
엔진이 코드를 실행하기 전엔 callStack이 비어있다. 하지만 코드가 작동을 시작하면 차레대로 push와 pop이 이루어지게 된다. 위 예제에서 맨 밑의 printSquare함수가 실행되면 callStack이 다음과 같이 변한다.
- printSquare함수 push
- printSqure함수에서 호출한 multiply함수 push
- multiply함수가 return값을 보냈으므로 pop
- printSqure함수에서 호출한 console.log함수 push
- console.log함수 작업 완료했으므로 pop
- printSquare함수 작업 완료했으므로 pop
여기서 callStack안의 함수를 각각 스택 프래임이라고 부른다
이 흐름을 이해했다면, 다음 예제를 살펴보며 callStack의 에러 처리와 오류 메세지 출력 방법을 알아보도록 하자.
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
해당 코드를 크롬에서 실행했다면 다음과 같은 오류 메세지를 볼수 있을것이다.
하지만 callStack은 정해진 크기가 있기때문에, 오버플로우도 일어날 수도 있다. 다음 예제를 살펴보자.
function foo () {
foo();
}
foo();
다음 예제에서, 코드는 재귀함수 형태로 자기 자신을 계속 호출중이다. 이러면 callStack은 다음과 같이 변하게된다.
다음 코드를 실행하면 foo() 함수는 계속해서 호출되고, 어느순간 주어진 callStack의 크기를 넘어서게 된다. 만약 그러한 오류가 발생되면 브라우저는 아래와 같은 오류를 발생시키고 함수를 중단시킨다.
WebAPI (Back Ground)
Web API는 javaScript엔진이 아니다. Web API는 브라우저에서 지원하는 API로 비동기 함수의 처리를 담당한다. Node.js에선 Back Ground라는 것이 같은 역할을 한다. 더 자세한 이해를 위하여 아래의 예제를 봐주길 바란다.
console.log("Start");
setTimeOut(function CB() {
console.log("Still Running");
}, 5000);
console.log("End");
다음은 이 프로그램을 실행했을때 콘솔창의 화면이다.
Start
End
Still Running
이상하지 않나? 왜 Still Running이 End보다 코드상에서 앞에 있었음에도 불구하고, 왜 End보다 이후에 출력된걸까? 그 이유는 callStack이 setTimeOut 작업을 webAPI로 넘기고 다른 함수들부터 처리했기 때문이다. 밑은 그 구조를 나타낸 그림이다.
setTimeout이 callStack에서 실행되면, webAPI로 비동기 작업을 넘긴다. setTimeout은 webAPI에 작업을 넘긴것만으로도할일을 다했다고 판단하기 때문에, 넘긴뒤에 Stack에서 나가고, 다음 작업인 "console.log("end")"를 실행한다. 만약 우리가 설정한 타이머의 시간인 5000ms(5s)가 지나게 되면, cb는 callback queue에 cb를 넘기게 된다.
callback queue( = test queue )
자, 설명하던것을 잠깐만 끊고 callback queue가 뭔지 알고가자. test queue라고도 불리는 callback queue는 말 그대로 callback함수들이 들어있는 queue이다. 여기서 queue는 자료구조중 하나로 FIFO(First In First Out) 즉, 처음 들어온 값이 처음으로 빠저나간다는 것이다. 자, 그럼 callback queue가 무엇인지도 짧게 알아봤으니 설명하던걸 이어가겠다.
아까 webAPI에서 보낸 cp함수가 callback queue에 도착했다. 그럼 callback queue는 일단 call stack이 비어있는 상태가 될때까지 대기한다. 이때, callback queue에 callback 함수의 여부와 call stack의 빈상태의 여부는 EventLoop가 판단한다.
EventLoop
EventLoop는 대기하다가, call stack이 비어있는것과 call stack에 callback함수가 있다는 것이 확인되면 "아 내가 할일이 생겼네"라 하며 callback queue의 함수들을 꺼내 callstack에 차곡차곡 쌓아준다. 그렇게 되면 드디어 콘솔에 "Still Running"이 찍히는 것이다.
여기까지 차근차근 읽었다면, setTimeout 0을 사용하는 이유가 무엇인지 알겠는가? 만약 추측이 되고, 맞다면 100%이해를 한 것이다. setTimeout 0를 사용하는 이유는 setTimeout에 들어있는 콜백 함수를 다음 루프때, 쉽게 말하면 나중에 실행하고 싶어서다.
예를들어, 위에 들었던 예제를 다음과 같이 바꾸어도 출력 시간의 차이 이외에는 실행 결과에는 차이가 없다.
console.log("Start");
setTimeOut(function CB() {
console.log("Still Running");
}, 0);
console.log("End");
그 이유는 CB함수는 결론적으론 callback queue에 들어갈 것이고, callstack에는 CB함수를 제외한 다른 함수들이 들어갈것인데, 어짜피 callstack이 빈 이후에 CB함수가 callstack에 들어갈 수 있을테니 말이다.
글을 마치며
이번글은 굉장히 쉽지 않았던 파트입니다. 아마 그렇기 때문에 제가 놓친부분이나 틀린 부분이 많을 겁니다. 만약 오류를 발견하시거나 피드백이 있으시면 자유롭게 말씀하시고, 반영하려 노력해보겠습니다.
참고 자료 및 출처
How JavaScript works: an overview of the engine, the runtime, and the call stack
What the heck is the event loop anyway? | Philip Roberts | JSConf EU
'BackEnd > Node.js & Express' 카테고리의 다른 글
[Node.js] Node.js와 그 특징 (0) | 2023.08.25 |
---|