How JavaScript works: 엔진, 실행환경, call stack 개요
이 글의 원문은 How JavaScript works: an overview of the engine, the runtime, and the call stack 입니다.
번역 과정에서 위 본문에 포함된 SessionStack에 대한 내용은 제외하였습니다.
자세한 내용은 위 링크를 통해 확인하시길 부탁 드립니다.
Intro
JavaScript의 인기가 늘어감에 따라 JavaScript가 front-end, back-end, hybrid apps, embadded devices 그리고 더 많은 영역을 지원할 수 있도록 활용분야를 넓혀가고 있다.
JavaScript를 만드는 것과 그것들이 어떻게 동작하는지 제대로 이해하는 것이 더 좋은 코드와 앱들을 만들 수 있게 한다고 생각했다.
GitHut stats에서 보는바와 같이 JavaScript의 active repositories와 전체 pushes 수는 Github에서 가장 높다. 다른 부분도 전혀 뒤쳐지지 않는다.
(2017년 4분기 GitHub language stats)
만약 당신의 프로젝트가 JavaScript를 많이 의존하고 있다면, 개발자는 언어와 생태계가 제공하는 내부의 것들을 깊고 또 깊게 이해하고 활용해야 훌륭한 소프트웨어를 만들 수 있다.
결과적으로 요즘에는 JavaScript를 이용하는 많은 개발자들이 있지만 아랫단에서는 어떤 일이 일어나는지에 대해 모르는 개발자들이 많다. (나도? ㅋ)
개요
대부분의 많은 사람들은 이미 V8 엔진의 개념은 들어봤을 것이고 JavaScript가 single-threaded 또는 callback queue를 사용한다는 것을 알고 있을 것이다.
이 포스트에서는 이러한 모든 개념을 자세하게, 그리고 JavaScript의 실제 작동을 설명할 것이다. 이러한 것을 자세히 알게 되면 APIs를 활용하는 non-blocking app을 더욱 잘 작성할 수 있다.
만약 JavaScript 초보라면, 이 포스트는 왜 JavaScript가 다른 언어와 비교하여 괴상야릇한지 이해하는데 도움이 될 것이다.
또는 JavaScript를 경험해본 개발자라면, JavaScript가 어떻게 실행되는지 신선한 통찰력을 얻을 것이다.
JavaScript 엔진
가장 인기 있는 JavaScript 엔진은 구글의 V8 이다. V8은 Node.js 와 크롬 내부에서 사용된다.
아래 그림은 V8 엔진을 간단히 한 것이다.
엔진은 2개의 메인 컴포넌트로 구성되어 있다.
- Memory Heap: 메모리 할당이 일어나는 부분
- Call Stack: 코드가 실행될 때 스택 프레임이 쌓이는 부분
실행환경
많은 JavaScript 사용자들이 사용하는 API들은 브라우저에 있다. (setTimeout 같은 것들). 그러나 이러한 API들은 엔진에서 제공되지 않는다.
그럼 이것들은 어디에서 올까?
이것들은 조금 더 복잡하다.
엔진은 실제로 좀 더 많은 부분이 있다. DOM, AJAX, setTimeout 과 같은 것들은 브라우저에서 제공하는 Web API라고 부른다.
Call Stack
JavaScript는 single-threaded 프로그래밍 언어이며 이것은 하나의 Call Stack을 사용함을 의미한다. 그러므로 한번에 하나의 일만 처리할 수 있다.
Call Stack은 프로그램의 어느 위치에 있는지를 기록하는 자료 구조이다. 만약 함수 안으로 들어가면 스택의 가장 윗 부분에 그 함수를 push한다. 만약 그 함수에서 나오면, 스택에서 가장 윗부분을 pop한다. 이것이 스택이 하는 모든 일이다.
예를 하나 보자.
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
엔진이 위 코드를 처음 실행할 때 Call Stack은 비어있는 상태이다. 그 다음은 아래와 같은 단계로 진행한다.
Call Stack 내의 각각의 entry는 Stack Frame이라고 부른다.
아래 코드는 exception이 발생해서 throw 됬을 때 실제 stack trace가 어떻게 구성되는지 보여준다.
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
만약 크롬에서 위 코드를 실행하면 다음과 같은 stack trace가 생성된다.
Stack overflow - 최대 Call Stack 사이즈에 도달하면 발생한다. 이것은 굉장히 쉽게 발생할 수 있다. 특히 재귀에서 종료 조건의 코드가 없이 수행하면 된다. 아래 코드를 보자.
function foo() {
foo();
}
foo();
엔진이 위 코드의 수행을 시작하면 함수 foo()
를 호출한다. 하지만 어떠한 종료 조건 없이 게속 재귀적으로 foo()
를 호출한다. 따라서 매 단계를 실행하면서 같은 함수가 Call Stack에 지속적으로 추가된다. 아래와 같이 말이다.
그러나 어느 시점에 Call Stack의 사이즈가 Call Stack의 최대치를 초과하게 되면 브라우저가 아래와 같은 에러를 발생시킨다.
single thread에서 실행하는 것은 multi-threaded 환경에서 발생하는 deadlock 과 같은 복잡한 문제를 처리하지 않아도 되므로 매우 편리하다.
그러나 single thread 또한 매우 제한적이다. JavaScript는 single Call Stack을 가지고 있는데 만약 느린 작업이 수행된다면 어떻게 될까?
동시성 & Event Loop
굉장히 오랜 시간이 소요되는 작업을 Call Stack에서 호출하는 일이 발생하면 어떻게 될까? 예를 들면 복잡한 이미지를 브라우저에서 JavaScript로 변환한다고 상상해 보아라.
아마 그것이 왜 문제가 되는지 궁금할 것이다. 문제는 Call Stack이 함수를 실행할 때 브라우저에서는 어떠한 일도 처리할 수 없게 된다. 즉, 모든 작업이 블록 된다. 이것은 브라우저가 화면을 표시할 수 없고, 다른 코드들을 실행할 수 없는 즉 멈춘다는 것을 의미한다. 이것은 또한 앱에서 나이스하게 부드러운 UI를 그리려고 할 때 문제가 된다.
그리고 이것 말고 문제가 또 있다. 브라우저에서 한번에 많은 작업들을 Call Stack에서 처리하려고 하면 아마도 오랜 시간동안 응답이 없을 것이다. 그리고 대부분 브라우저들은 해당 웹 페이지를 에러로 인해 종료할 것인지 사용자에게 물어볼 것이다.
이것은 훌륭한 UX가 아니다. 별로다.
그럼 우리는 UI를 멈추지 않고 브라우저가 잘 반응 하도록 어떻게 무거운 코드들을 실행할 수 있을까? 그 해결책은 asynchronous callbakcs에 있다.
이 부분은 다음 포스팅에서 다루도록 하겠다.