JavaScript Runtime
런타임이란 프로그래밍 언어가 구동되는 환경이다. 환경은 프로그램의 일종으로서 런타임이란 '프로그래밍 언어가 동작할 수 있는 프로그램'을 의미한다. 우리가 작성한 자바스크립트 코드가 우리가 원하는 동작을 하려면 코드가 실행되어야한다. 자바스크립트를 이해하기 위해서 자바스크립트 런타임에 대한 이해는 필수적이다.
대표적으로 자바스크립트 코드를 실행할 수 있는 런타임에는 브라우저가 있다. 브라우저 자바스크립트 런타임을 도식화하면 다음과 같다(Figure 1). Node.js 런타임도 동일한 구조에 Web API만 C++ API로 대체된 모양이다.
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)
를 쌓는다.
그리고 쌓였던 실행 컨텍스트들은 각 함수가 리턴할 때 스택에서 제거된다. 위 그림에서 현재 실행중인 코드는 스택의 맨 위에 있는 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')
함수가 스택에 쌓였다가 실행이 종료되면 사라진다.
그리고 나서 두 번째 함수 호출인 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에 쌓인다.
setTimeout()
은 그야말로 타이머를 설정하는 함수로서 즉시 실행되고 call stack에서 사라진다.
콜백이 5초간 기다리는지는 call stack이 아니라 WebAPI에서 관리한다. 덕분에 setTimeout(cb)
다음에 나오는 console.log('JSConfEU')
와 같이 단순한 작업이 5초씩이나 기다리지 않고 바로 실행될 수 있다.
console.log('JSConfEU')
까지 모두 실행시키면 프로그램 전체가 실행되었으므로 main()
까지 call stack에서 제거되고 call stack이 비게된다.
한편 5초간 대기가 끝난 cb 함수는 task queue로 옮겨간다. 실행될 준비가 되었다는 뜻이다.
task queue에 있는 작업들은 call stack이 비어 있을 때 큐에 들어온 순서대로 실행된다. 이 때 위 그림에서 계속해서 루프를 돌면서
- call stack이 비었는지를 수시로 확인하고
- task queue에 있는 작업들을 하나씩 stack으로 옮겨주는 작업
을 하는 것이 바로 event loop다.
이제 최종적으로 cb()
이 실행되고 콘솔에 there
이 찍히고 나면 calls tack과 task queue가 모두 비게된다.
Callback Queue
🔗 JavaScript Runtime - Event Loop, Callback Queue