Javascript Runtime 의 구조

language | 17 May 2018

Tags | javascript nodejs runtime promise eventloop

javascript 언어는 다른 많은 언어들과 다르게 굉장히 다양한 Runtime(=실행환경) 속에서 실행된다.

다른 언어 예를 들자면, python 은 python 공식 interpreter 가 주기적으로 업데이트되며, 모든 파이썬 코드 작성자는 python 공식 interpreter 를 이용한다. java 는 oracle 에서 JDE 를 공식 배포 및 관리하고, java 언어 사용자들은 모두 같은 java Runtime 을 이용하고 개발한다.

반면에 javascript 는 개발자가 코드를 짜도, Runtime 에 따라 작동 양상이 다를 수 있다.

그럼에도 모든 javascript Runtime 들은 비슷한 구조와 동작방식을 가지고 있다. 가장 대중적인 js runtime 인 nodejs 부터 브라우저에 내장된 runtime 까지, 기본 틀은 비슷하다.

그리고 javascript runtime 들의 공통적인 동작방식을 아는 것은 javascript 코드를 짜는데에 매우 도움이 된다. 만약 callback Pattern 의 코드에서 Promise, async function, await Pattern 의 코드로 넘어가고 싶어도, javascript runtime 의 동작방식을 모르면 쉽사리 넘어갈 수 없다.

그래서 이번 글에서는 Javascript Runtime 의 공통적인 구조를 파악해보려고 한다.

Runtime 기본 4 요소

Javascript Runtime 는 큰 4가지 요소로 나눌 수 있는데 이는 아래와 같다.

  1. Javascript Engine ( Call Stack , Memory Heap )
  2. Background
  3. 여러 Task Queues 집단 ( Task Queue, Micro Task Queue, + Animation Frame 등 )
  4. Event Loop

Javascript Engine : 코드 인터프리터

JS Engine 은 .js파일을 읽으면서 Call stack 을 채우고, Call stack 에 있는 작업들을 수행하기를 반복한다.

엔진에 따라 외부 모듈을 읽어들이는 과정도 (require('https') or import 'https') 수행한다.

Background : 수많은 작업을 하는 멀티 Thread

JS Engine 에서 코드를 수행하다가, 다음과 같은 Background API 를 호출하게 되면, Background 작업이 시작된다.

  1. Timer 작업 (setTimeout(cb, 10 , ...args) 등으로 호출)
  2. eventListener 작업 (onClick(cb, ...args) 등으로 호출)
  3. Promise

각 Background 작업을 마치면, API가 호출될 때 전달받은 callback(Task) 을 Task Queue 에 삽입하기 위해 Event Loop 에게 전달한다.

작업 callback 전달
Timer 작업 timer 동작이 끝나면 함께 전달받은 callback 을 전달한다
eventListener 작업 이벤트를 감지하면 전달받은 callback 을 전달한다
Promise Promise 안에 담긴 작업을 수행 완료하면 .then() 로 전달받은 callback 을 전달한다

Promise 의 동작은 따로 정리하는 것이 좋을 것 같아서 대강 설명하였다.

바로 다음 post 에서 정리하겠다.

Event Loop : call stack 과 background 의 중개자

이번엔 Event Loop 가 하는 일을 알아보겠다. Event Loop 는 Javascript Runtime 의 중심에서 Call Stack 과 Background 간의 업무 처리를 돕는 중개자 역할을 한다.

무한 루프를 돌면서 Callback(Task) 를 Background 에서 Task Queue 로, Task Queue 에서 Engine 의 Call stack 으로 적절히 전달한다.

참고 : 단일/멀티 Thread

Javascript Engine 은 단일 Thread 이다.

“단일 Thread” 라는 말과 “Call Stack 이 하나” 라는 말은 같은 말이다.

요소 Thread 특징
Javascript Engine 단일 Thread javascript 코드를 한 줄씩 읽으면서 작업을 수행한다.
Background (일종의) 멀티 Thread Engine 이 코드를 수행하는 동안, API 호출을 통해 전달받은 작업을 멀티 Thread 에서 동시다발적으로 수행한다.
Event Loop 단일 Thread 단일 Thread 위에서 무한 루프가 돌며 나머지 3요소를 관리한다.

Promise 로 부터 전달된 Micro Task 는 우선 처리

Task Queue 부분은 여러 종류의 Task Queue 를 가지고 있는데, Javascript 의 Promise 문법을 지원하는 최근의 javascript runtime 들은 Micro Task Queue 를 가진다.

Background 의 Promise 로부터 전달된 callback(task)는 Micro Task Queue 에 쌓이고, JS Engine 의 Call Stack 이 비었을 때 Task Queue 보다 우선적으로 Micro Task Queue 의 task 를 먼저 수행한다.

아래는 그 예시이다.


console.log("1") ;

setTimeout( _ => {console.log("setTimeout");}, 0) ;

console.log("2") ;

Promise.resolve().then(_ => {
  console.log("promise1");
}).then( _ => {
  console.log("promise2");
});

console.log("3") ;

이 코드를 실행시키면 콘솔엔 아래와 같이 찍힌다.

1
2
3
promise1
promise2
setTimeout

1,2,3 가 먼저 찍힌 것은 javascript engine 이 setTimeout 이나 Promise.resolve().then(...) 처럼 Background 의 작업을 기다리지 않고 (비동기적) 다음 코드를 우선적으로 실행하는 것을 보여준다.

코드가 실행되는 동안 event loop 와 background 에서는 열심히 timer 작업과 promise 작업을 수행할 것이고, call stack 이 다 비워진 시점에 micro task queue 와 task queue 는 아래와 같을 것이다.

Micro Task Queue Task Queue
_ => {console.log("promise1");} _ =>{console.log("setTimeout");}
_ => {console.log("promise2");}  

이 때 micro task queue 에 있는 task(callback) 들이 우선적으로 engine 의 call stack 으로 (event loop 에 의해) 전달되고, 우선적으로 수행된다.

micro task queue 가 다 비워진 후에는 task queue 의 작업들이 옮겨지고 수행된다.