Node.js 가 등장하기 전 javascript 코드는 브라우저에서만 동작할 수 있었습니다.
javascript 코드가 동작하려면 어떤 환경이 갖춰져야 하길래 브라우저에서만 동작할 수 있었는지 그리고 javascript 코드는 어떤 방식으로 동작하는지에 대해 정리해보겠습니다.
Javascript 실행환경
javascript 코드는 특정한 실행환경이 갖춰져야 동작이 가능합니다. 그 실행환경은 아래 4가지 요소로 구성됩니다.
- javascript 엔진
- Web API / Node API
- callback 큐
- 이벤트 루프
먼저, javascript 엔진에 대해 알아보겠습니다. javascript 엔진은 javascript 코드를 읽고 실행시키는 프로그램입니다. 위 사진에서 JS, Memory Heap, Call Stack으로 이루어진 큰 네모 박스가 javascript 엔진입니다.
javascript 엔진에는 코드를 실행하는데 필요한 Memory Heap과 Call Stack이 같이 들어있습니다. Memory Heap에는 non-primitive 타입의 데이터가 저장되고 call stack에는 함수 호출 시 생성되는 execution context(실행 컨텍스트)가 stack 형태로 쌓이며 저장됩니다.
execution context에는 함수의 argument, 함수에서 선언한 변수 등 함수가 실행되면서 필요한 정보가 저장됩니다. execution context는 함수 호출 시 생성되고 함수가 종료되면 사라집니다. 따라서 execution context가 실행 중인 함수를 의미한다고 볼 수도 있습니다. javascript 엔진은 call stack의 제일 위에 올라와 있는 함수부터 하나씩 실행합니다.
다음은 Web API / Node API입니다. DOM 객체나 setTimeout 같은 함수들은 javascript 언어 자체에서 지원하는 기능이 아닌 javascript 실행환경에서 제공하는 기능입니다. 여기서 다른 Web API 들을 확인할 수 있습니다. 여기에서는 Node API 들을 확인할 수 있습니다. 브라우저 환경에서는 Web API가 제공되고 node.js 환경에서는 Node API가 제공됩니다.
세 번째는 callback 큐 입니다. callback 큐는 callback 함수들이 실행되기 전 대기하는 공간입니다. callback 함수들은 callback 큐에서 대기하다가 call stack이 비게 되면 call stack에 하나씩 쌓이고 실행이 됩니다. callback 큐는 이름에서도 알 수 있듯이 FIFO(First-In-First-Out) 형태로 동작합니다.
마지막으로 이벤트 루프입니다. callback 큐와 call stack을 주시하면서 callback 큐에서 대기중인 callback 함수를 call stack이 비었을 때 call stack에 쌓아주는 역할을 합니다.
javascript 코드는 이렇게 네 가지의 구성요소가 있어야 실행이 가능합니다. 모든 브라우저는 위 네 가지의 구성요소가 내장되어 있어서 javascript 코드를 실행할 수 있는 것입니다. Node.js는 브라우저 밖에서도 javascript 코드를 실행할 수 있도록 위 네 가지 구성요소를 브라우저 밖에 구현해놓은 실행환경입니다. 다만, node.js는 브라우저에서 필요한 Web API 대신 Node API를 제공합니다. 같은 javascript 언어이지만 node.js에서는 document 객체가 존재하지 않는 이유가 바로 Node API에서 DOM 객체 기능을 제공하지 않기 때문입니다.
Javascript 동작방식
그럼 javascript 코드가 동작하려면 왜 이런 실행환경을 갖추고 있어야 할까요?
그 이유는 javascript가 single thread에서 동작하는 언어이기 때문입니다. single thread에서 동작하는 언어라는 말은 하나의 call stack만 가지고 있으며 한 번에 한 가지 일만 처리할 수 있다는 것을 의미합니다. 여기서 문제가 발생합니다. javascript는 한 번에 한 가지 일만 처리할 수 있지만 javascript가 처리하는 브라우저의 이벤트는 한 번에 한 가지씩만 발생하지 않습니다. 그렇기 때문에 single thread로 비동기적으로 발생하는 이벤트를 처리할 수 있어야 합니다.
Single thread로 비동기적인 이벤트를 처리하기 위해 등장한 것이 바로 callback 함수입니다. addEventListener나 setTimeout 같은 함수의 경우 callback 함수를 넘겨줍니다. 그리고 이벤트가 발생되거나 지연시간이 종료되어 callback 함수가 호출될 때 바로 call stack에 쌓이는 것이 아니라 callback 큐에 들어가 call stack이 빌 때까지 대기합니다. 그리고 call stack이 비게 되면 그때 callback 함수가 call stack에 쌓이고 실행이 됩니다. 이런 방식으로 javascript는 single thread에서 비동기적인 이벤트를 처리합니다.
여기서 call stack이 비어있는지를 확인하는 역할을 이벤트 루프가 하게 됩니다. 이벤트 루프는 callback 큐에 대기 중인 callback 함수가 있는지 확인합니다. 대기 중인 callback 함수가 있다면 call stack이 비어있는지 계속해서 확인합니다. call stack이 비게 되면 대기 중이던 callback 함수를 call stack에 쌓아줍니다.
이제 코드를 보면서 javascript 코드가 실행될 때 call stack과 callback 큐가 어떻게 변하는지 살펴보겠습니다.
function hello3() {
return "Jony";
}
function hello2() {
return `my name is ${hello3()}`;
}
function hello1() {
return `Hello, ${hello2()}`;
}
hello1(); // Hello, my name is Jony
위 코드가 실행되는 동안 call stack에서는 아래 그림과 같은 일들이 일어납니다.
함수가 호출되는 순서대로 call stack에 쌓이고 javascript 엔진은 제일 위에 있는 함수부터 하나씩 처리합니다. 만약, 위 코드에서 hello3() 함수가 처리시간이 오래 걸리는 함수라면 hello3() 함수가 종료될 때까지 hello1(), hello2() 함수는 계속해서 대기해야 합니다.
이번에는 callback 함수가 포함된 코드를 살펴보겠습니다.
setTimeout(function cb1() {
console.log(1);
}, 0);
console.log(2);
setTimeout(function cb2() {
console.log(3);
}, 0);
// 2
// 1
// 3
위 코드가 실행되는 동안 아래의 과정이 일어납니다.
먼저, cb1() 함수를 콜백으로 받은 setTimeout(cb1, 0) 함수가 호출되고 call stack에 쌓입니다.
call stack에 쌓인 setTimeout(cb1, 0) 함수가 실행되면 Web API에 있는 timer가 지연시간만큼 callback 함수를 대기시킵니다. 위 코드에서는 지연시간이 0이므로 0ms 만큼 대기합니다. 지연시간이 0ms라 하더라도 아주 잠깐 대기 후에 callback 큐에 들어가게 됩니다.
이제 다음 함수인 console.log(2) 함수가 call stack에 쌓입니다. 그리고 timer가 종료되면 callback 함수가 callback 큐로 들어갑니다. 위 코드에서는 지연시간이 0ms 이므로 거의 바로 callback 큐에 들어갑니다.
console.log(2) 함수가 실행되고 call stack에서 제거되면 다음 함수인 cb2() 함수를 callback으로 받은 setTimeout(cb2, 0) 함수가 call stack에 쌓입니다. console.log(2) 함수가 제거되고 setTimeout(cb2, 0) 함수가 call stack으로 들어오기 전 call stack이 잠깐 비어있게 되지만 새로 들어오는 함수의 우선순위가 더 높기 때문에 callback 큐에 대기중인 callback 함수들은 더 이상 call stack에 새로 들어오는 함수가 없을 때까지 대기합니다.
setTimeout(cb2, 0) 함수가 실행되면 마찬가지로 timer가 0ms 만큼 cb2를 대기시킵니다.
0ms가 지나면 cb2 역시 callback 큐로 들어갑니다.
더 이상 call stack에 새로 들어오는 함수가 없고 call stack이 비었기 때문에 이제 callback 큐에서 대기 중이던 callback 함수들이 차례대로 call stack에 쌓입니다. 제일 처음 callback 큐로 들어왔던 cb1() 함수가 call stack에 쌓이고 cb1() 함수 내부에서 호출한 console.log(1) 함수도 이어서 call stack에 쌓입니다.
console.log(1) 함수와 cb1() 함수가 종료되고 call stack에서 제거되면 다시 call stack이 비어있게 됩니다.
이어서 callback 큐에서 대기 중이던 cb2() 함수가 call stack에 쌓이고 cb2() 함수 내부에서 호출한 console.log(3) 함수도 이어서 call stack에 쌓입니다. 이 두 함수가 종료되고 call stack에서 제거되면 javascript 코드의 실행이 종료됩니다.
위 과정에서 알 수 있듯이 javascript에서는 비동기 처리가 필요한 경우 callback 함수를 받고 그 callback 함수들은 callback 큐에서 대기합니다. 그리고 event loop가 javascript 코드 내의 모든 동기 함수들의 처리가 끝난 후 call stack이 비었을 때 callback 큐에서 대기 중인 callback 함수들을 하나씩 call stack에 쌓아줍니다. 이런 방식을 통해 javascript는 single thread를 이용하여 비동기 이벤트들을 처리합니다. javascript는 동시에 한 가지 일만 진행할 수 있지만 이런 과정들이 매우 빠르게 일어나기 때문에 여러 가지의 비동기적인 이벤트들을 동시에 처리하는 것처럼 보이게 됩니다.
'Javascript' 카테고리의 다른 글
Javascript의 callback과 promise (0) | 2021.05.19 |
---|---|
Javascript의 iterable, iterator, generator (0) | 2021.04.06 |
프로토타입 체인을 이용한 Javascript의 상속 (0) | 2021.02.21 |
Javascript의 class (0) | 2021.02.08 |
Javascript의 "this" (0) | 2021.01.21 |