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 의 공통적인 구조를 파악해보려고 한다.
Javascript Runtime 는 큰 4가지 요소로 나눌 수 있는데 이는 아래와 같다.
구성 요소들을 하나하나 살펴보자.
Node Js 나 브라우저와 같은 Javascript Runtime 에 포함되어있는 Javascript Engine 은 코드를 읽는 Interpreter, 코드를 바탕으로 현재 실행중인 서브루틴을 관리하는 Call Stack, 그리고 변수와 객체에 대한 메모리 할당 및 관리를 담당하는 Heap Memory 로 구성되어있다.
자바스크립트 엔진은 은 .js
파일을 읽으면서 Call stack 을 채우고, Call stack 에 있는 작업들을 수행하기를 반복한다.
엔진에 따라 외부 모듈을 읽어들이는 과정도 수행한다. (ex :
require('https')
,import 'https'
)
JS Engine 에서 코드를 수행하다가, 다음과 같은 Background API 를 호출하게 되면, Background 작업이 시작된다.
setTimeout(cb, 10 , ...args)
등으로 호출)onClick(cb, ...args)
등으로 호출)각 Background 작업을 마치면, API가 호출될 때 전달받은 callback(Task) 을 Task Queue 에 삽입하기 위해 Event Loop 에게 전달한다.
작업 | callback 전달 |
---|---|
Timer 작업 | timer 동작이 끝나면 함께 전달받은 callback 을 전달한다 |
eventListener 작업 | 이벤트를 감지하면 전달받은 callback 을 전달한다 |
Promise | Promise 안에 담긴 작업을 수행 완료하면 .then() 로 전달받은 callback 을 전달한다 |
Event Loop 는 Javascript Runtime 의 중심에서 Call Stack 과 Background 간의 업무 처리를 돕는 중개자 역할을 한다. 무한 루프를 돌면서 Callback(Task) 를 Background 에서 Task Queue 로, Task Queue 에서 Engine 의 Call stack 으로 적절히 전달한다. Single Thread 로 구성되어있고, Background 의 작업 수행을 기다리지 않기 때문에 Non-Blocking 의 특징을 가지고 있다.
Javascript Engine 과 Event Loop 는 단일 Thread 이고, Background 는 (사실상) Multi Thread 이다.
요소 | Thread | 특징 |
---|---|---|
Javascript Engine | 단일 Thread | javascript 코드를 한 줄씩 읽으면서 작업을 수행한다. |
Background | (일종의) 멀티 Thread | Engine 이 코드를 수행하는 동안, API 호출을 통해 전달받은 작업을 멀티 Thread 에서 동시다발적으로 수행한다. |
Event Loop | 단일 Thread | 단일 Thread 위에서 무한 루프가 돌며 나머지 3요소를 관리한다. |
다음은 자바스크립트 코드가 Runtime 에서 어떻게 수행되는지 순서대로 나타낸 것이다.
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 의 작업들이 옮겨지고 수행된다.