Javascript Runtime 의 구조

language | 17 May 2018

Tags | javascript nodejs runtime promise eventloop

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

다른 언어 예를 들자면, Python 은 공식 interpreter 가 주기적으로 업데이트되며, 모든 파이썬 코드 작성자는 Python 공식 Interpreter 를 이용한다. Java 또한 공식 JDK 가 배포 및 관리되고, 자바 개발자는 다른 개발자들이 모두 JVM 이라는 동일한 Java Runtime 을 이용한다고 가정하고 개발한다.

반면에 Javascript 는 같은 코드여도, Runtime 에 따라 실행 양상이 다를 수 있다.

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

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

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

🏁 Runtime 기본 4 요소

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

  1. Javascript Engine : Call Stack , Heap Memory
  2. Background : 비동기 task 를 수행하는 Multi Threads
  3. 여러 Task Queue : Background 작업 수행 후 Callback 을 관리
  4. Event Loop : Single Thread, Non-blocking IO 의 책임

구성 요소들을 하나하나 살펴보자.

요소 1. Javascript Engine

Node Js 나 브라우저와 같은 Javascript Runtime 에 포함되어있는 Javascript Engine 은 코드를 읽는 Interpreter, 코드를 바탕으로 현재 실행중인 서브루틴을 관리하는 Call Stack, 그리고 변수와 객체에 대한 메모리 할당 및 관리를 담당하는 Heap Memory 로 구성되어있다.

자바스크립트 엔진은 은 .js파일을 읽으면서 Call stack 을 채우고, Call stack 에 있는 작업들을 수행하기를 반복한다.

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

요소 2,3. Background 와 Task Queue

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 을 전달한다

요소 4. Event Loop

Event Loop 는 Javascript Runtime 의 중심에서 Call Stack 과 Background 간의 업무 처리를 돕는 중개자 역할을 한다. 무한 루프를 돌면서 Callback(Task) 를 Background 에서 Task Queue 로, Task Queue 에서 Engine 의 Call stack 으로 적절히 전달한다. Single Thread 로 구성되어있고, Background 의 작업 수행을 기다리지 않기 때문에 Non-Blocking 의 특징을 가지고 있다.

참고 : 요소별 단일/멀티 Thread

Javascript Engine 과 Event Loop 는 단일 Thread 이고, Background 는 (사실상) Multi Thread 이다.

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

🖼 그림으로 알아보는 Javascript Runtime 동작

다음은 자바스크립트 코드가 Runtime 에서 어떻게 수행되는지 순서대로 나타낸 것이다.

📌 놓치기 쉬운 포인트 : 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 의 작업들이 옮겨지고 수행된다.