이 글의 원문은 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에서 가장 높다. 다른 부분도 전혀 뒤쳐지지 않는다.
[IMAGE: https://steemitimages.com/DQmXeWhCkbw2NZnh1czZEcRuYakU29HYJpLwngK6n6wyBza/1.png]
(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 엔진을 간단히 한 것이다.
[IMAGE: https://steemitimages.com/DQmTwwXb5VAHiQqhypZZcH4N16DdEeX3cTw8q1Vm3j8r8Tk/2.png]
엔진은 2개의 메인 컴포넌트로 구성되어 있다.
- Memory Heap: 메모리 할당이 일어나는 부분
- Call Stack: 코드가 실행될 때 스택 프레임이 쌓이는 부분
실행환경
많은 JavaScript 사용자들이 사용하는 API들은 브라우저에 있다. (setTimeout 같은 것들). 그러나 이러한 API들은 엔진에서 제공되지 않는다.
그럼 이것들은 어디에서 올까?
이것들은 조금 더 복잡하다.
[IMAGE: https://steemitimages.com/DQmWCgFrX3jzouquz4zY1Y7r7HNpneA5jfsXcJgmGi8a7Ca/3.png]
엔진은 실제로 좀 더 많은 부분이 있다. 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은 비어있는 상태이다. 그 다음은 아래와 같은 단계로 진행한다.
[IMAGE: https://steemitimages.com/DQmSFf3ZgjNokmo1Pg46NPgThv1bjiE1i1itbuLYJLURb3n/4.png]
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가 생성된다.
[IMAGE: https://steemitimages.com/DQmW3DwkH6kkQQ7pcuVQi2kQPzzFDyRGpDNNzkUwRBKGgp8/5.png]
Stack overflow - 최대 Call Stack 사이즈에 도달하면 발생한다. 이것은 굉장히 쉽게 발생할 수 있다. 특히 재귀에서 종료 조건의 코드가 없이 수행하면 된다. 아래 코드를 보자.
function foo() {
foo();
}
foo();
엔진이 위 코드의 수행을 시작하면 함수 foo() 를 호출한다. 하지만 어떠한 종료 조건 없이 게속 재귀적으로 foo()를 호출한다. 따라서 매 단계를 실행하면서 같은 함수가 Call Stack에 지속적으로 추가된다. 아래와 같이 말이다.
[IMAGE: https://steemitimages.com/DQmcKuRcNfjSCZRbxiZ2sXBKE1Hd3YMZmmoBSEHVcLhp6nb/6.png]
그러나 어느 시점에 Call Stack의 사이즈가 Call Stack의 최대치를 초과하게 되면 브라우저가 아래와 같은 에러를 발생시킨다.
[IMAGE: https://steemitimages.com/DQmaQP5SPyA5vSBmG6bKqk3TaKEv7SYMpceahV5Yr8YouGN/7.png]
single thread에서 실행하는 것은 multi-threaded 환경에서 발생하는 deadlock 과 같은 복잡한 문제를 처리하지 않아도 되므로 매우 편리하다.
그러나 single thread 또한 매우 제한적이다. JavaScript는 single Call Stack을 가지고 있는데 만약 느린 작업이 수행된다면 어떻게 될까?
동시성 & Event Loop
굉장히 오랜 시간이 소요되는 작업을 Call Stack에서 호출하는 일이 발생하면 어떻게 될까? 예를 들면 복잡한 이미지를 브라우저에서 JavaScript로 변환한다고 상상해 보아라.
아마 그것이 왜 문제가 되는지 궁금할 것이다. 문제는 Call Stack이 함수를 실행할 때 브라우저에서는 어떠한 일도 처리할 수 없게 된다. 즉, 모든 작업이 블록 된다. 이것은 브라우저가 화면을 표시할 수 없고, 다른 코드들을 실행할 수 없는 즉 멈춘다는 것을 의미한다. 이것은 또한 앱에서 나이스하게 부드러운 UI를 그리려고 할 때 문제가 된다.
그리고 이것 말고 문제가 또 있다. 브라우저에서 한번에 많은 작업들을 Call Stack에서 처리하려고 하면 아마도 오랜 시간동안 응답이 없을 것이다. 그리고 대부분 브라우저들은 해당 웹 페이지를 에러로 인해 종료할 것인지 사용자에게 물어볼 것이다.
[IMAGE: https://steemitimages.com/DQmTdazGTiRbfLfmMg3UqgCywYPaae1QBAT9hxDkZfkKpUa/8.jpeg]
이것은 훌륭한 UX가 아니다. 별로다.
그럼 우리는 UI를 멈추지 않고 브라우저가 잘 반응 하도록 어떻게 무거운 코드들을 실행할 수 있을까? 그 해결책은 asynchronous callbakcs에 있다.
이 부분은 다음 포스팅에서 다루도록 하겠다.