JavaScript Runtime

런타임이란 프로그래밍 언어가 구동되는 환경이다. 환경은 프로그램의 일종으로서 런타임이란 '프로그래밍 언어가 동작할 수 있는 프로그램'을 의미한다. 우리가 작성한 자바스크립트 코드가 우리가 원하는 동작을 하려면 코드가 실행되어야한다. 자바스크립트를 이해하기 위해서 자바스크립트 런타임에 대한 이해는 필수적이다.

대표적으로 자바스크립트 코드를 실행할 수 있는 런타임에는 브라우저가 있다. 브라우저 자바스크립트 런타임을 도식화하면 다음과 같다(Figure 1). Node.js 런타임도 동일한 구조에 Web API만 C++ API로 대체된 모양이다.

[Figure1] Zlatkov, How JavaScript works: an overview of the engine, the runtime, and the call stack

Memory Heap

const a = 1과 같이 선언한 변수들이 저장되는 곳이다.

🚧 Coming Soon...

Call Stack

Memory Heap이 변수가 저장되는 곳이라면 call stack은 함수들이 쌓이는 곳이다. 쌓였던 스택은 함수 리턴시에 제거된다. 이 call stack을 통해 JavaScript 엔진은 프로그램을 구성하는 코드를 어떤 순서로 실행시켜야할지를 파악한다.

예를 들어 다음과 같은 아주 간단한 프로그램 코드가 작성된 main.js를 실행시킨다고하자.

function multiply (a, b) {
  return a + b
}

function square (n) {
  return multiply(n, n);
}

function multiply (a, b) {
  var squared = square(n);
  console.log(squared);
}

printSquare(4);

call stack에는 가장 먼저 js 파일, 즉 프로그램 자체에 대한 스택이 가장 먼저 쌓인다. 편의상 이것을 main()이라는 함수 호출이 스택에 쌓인 것으로 이해하자. 그리고 나서 main .js 파일을 위에서부터 아래로 쭉 읽어내려가다보면 최초의 함수 실행인  printSquare(4)를 만나게 된다. 그러면 main()위에 printSquare(4) 를 쌓는다.      printSquare(4)는 내부적으로 square(4)를 호출하고 있으므로 printSquare(4) 위에 square(4)를 쌓는다. 그리고square(4)는 또다시 multiply(4, 4)를 호출하고 있으므로 square(4) 위에 multiply(4, 4)를 쌓는다.

[Figure 2] Roberts, "What the heck is the event loop anyway?"

그리고 쌓였던 실행 컨텍스트들은 각 함수가 리턴할 때 스택에서 제거된다.  위 그림에서 현재 실행중인 코드는 스택의 맨 위에 있는 multiply(4, 4)다. multiply(4, 4)에서 return을 만나면 스택에서 multiply(4, 4)가 제거된다. 같은 방법으로 suare(4), printSquare(4)를 콜 스택에서 제거하고 나면 main.js 프로그램을 모두 실행시켰으므로 main()도 스택에서 제거되고 최종적으로 call stack이 비게된다.

흔히들 자바스크립트가 single-thread 기반이라고 말한다. 이 말은 곧 자바스크립트에는 명령을 처리하는 call stack이 하나밖에 없어서 한 번에 한 가지 일 밖에 못한다는 것을 의미한다.

Web API

자바스크립트가 기본적으로 제공하는 문법들 이외에 브라우저 런타임은 브라우저 환경에서 필요한 기능들을 추가적으로 제공하는데, 이를 Web API라고 부른다. 자바스크립트가 화면 내 HTML 요소들에 접근할 수 있도록 해주는 인터페이스인 DOM, 서버에 요청을 보내 필요한 리소스를 받아올 수 있게 해주는 fetch와 같은 문법이 Web API에 속한다. 같은 자바스크립트 런타임이라도 Node.js는 사용자에게 화면을 보여주는 것을 목표로하지 않기때문에 Web API를 제공하지 않는다. 따라서 Node.js 런타임은 document.createElement('div')와 같은 코드를 이해하지 못한다.

C++ API

Node.js는 브라우저 런타임과 달리 Web API 대신 C++ API를 제공한다

Event Loop

자바스크립트 콜 스택이 하나라고해서 단순한 연산에서부터 시간이 오래 걸리는 네트워크 요청까지 모든 작업들을 call stack에서 순차적으로 처리해 나간다면 우리는 네트워크 요청이 일어날때마다 클릭, 스크롤 등의 인터랙션을 포함해 그 어떤 다른 일도 할 수 없을 것이다. 하지만 우리는 분명 유튜브 동영상이 다 로드될때까지도 기다리지 못해 그 사이에 스크롤을 내려 댓글을 확인하고 다시 스크롤을 올려 동영상이 다 로드되었는지를 확인한 적이 있다. 자바스크립트가 single-thread 기반이라면 오늘날 이러한 유려한 브라우저 경험은 어떻게 가능한 것일까?

그건 바로 event loop를 통해 시간이 많이 걸리는 작업들의 실행 순서를 조정할 수 있기 때문이다. 예를들어

setTimeout(function cb() {
  console.log('there
}, 5000)

console.log('JSConfEU')

이렇게 생긴 코드를 실행한다고하자. 먼저 console.log('Hi') 함수가 스택에 쌓였다가 실행이 종료되면 사라진다.

[Figure 3] Roberts, "What the heck is the event loop anyway?"

그리고 나서 두 번째 함수 호출인 setTimeout(cb)을 만난다.setTimeout()은 WebAPI의 일부로서  setTimeout()의 인자로 들어오는 console.log('there')는 지금 당장 실행되는게 아니라 두번째 인자인 5000ms만큼 기다렸다가 실행된다.  setTimeout()이 call stack에서 5초간 대기한다면  웹 사이트에서는 5초동안 아무것도 못한다. 그 동안 사용자는 클릭이나 스크롤과 같은 상호작용을 전혀 할 수 없어 불쾌한 사용자 경험으로 이어진다. 이처럼 시간이 오래 걸리는 작업이 이후 작업의 실행을 막는 현상을 블로킹(blocking)이라고 한다. 이러한 블로킹을 방지하기 위해(non-blocking) JavaScript 엔진은 setTimeout()이나 네트워크 요청(fetch())처럼 시간이 오래 걸리는 작업들을 call stack 말고 다른 곳에서 기다리도록하고 대기 시간이 끝나거나 서버에서 응답을 받으면 다시 call stack에서 처리한다. 이렇게 블로킹 없는(non-blocking) 작업 분배를 가능하게 해주는 것이 바로 event loop다.

먼저 setTimeout(cb)도 다른 함수들과 마찬가지로 먼저 call stack에 쌓인다.

[Figure 4] Roberts, "What the heck is the event loop anyway?"

setTimeout()은 그야말로 타이머를 설정하는 함수로서 즉시 실행되고 call stack에서 사라진다.

[Figure 5] Roberts, "What the heck is the event loop anyway?"

콜백이 5초간 기다리는지는 call stack이 아니라 WebAPI에서 관리한다. 덕분에 setTimeout(cb) 다음에 나오는 console.log('JSConfEU') 와 같이 단순한 작업이 5초씩이나 기다리지 않고 바로 실행될 수 있다.

[Figure 6] Roberts, "What the heck is the event loop anyway?"

console.log('JSConfEU') 까지 모두 실행시키면 프로그램 전체가 실행되었으므로 main()까지 call stack에서 제거되고 call stack이 비게된다.

[Figure 7] Roberts, "What the heck is the event loop anyway?"

한편 5초간 대기가 끝난 cb 함수는 task queue로 옮겨간다. 실행될 준비가 되었다는 뜻이다.

[Figure 8] Roberts, "What the heck is the event loop anyway?"

task queue에 있는 작업들은 call stack이 비어 있을 때 큐에 들어온 순서대로 실행된다. 이 때 위 그림에서 계속해서 루프를 돌면서

  • call stack이 비었는지를 수시로 확인하고
  • task queue에 있는 작업들을 하나씩 stack으로 옮겨주는 작업

을 하는 것이 바로 event loop다.

[Figure 9] Roberts, "What the heck is the event loop anyway?"

이제 최종적으로 cb()이 실행되고 콘솔에 there 이 찍히고 나면 calls tack과 task queue가 모두 비게된다.

Callback Queue

🔗 JavaScript Runtime - Event Loop, Callback Queue

References

Subscribe to go-kahlo-lee

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe