An overview of the engine, the runtime, and the call stack

이 글은 How JavaScript works: an overview of the engine, the runtime, and the call stack 을 번역한 글입니다.

들어가기

자바스크립트의 인기가 나날이 증가함에 따라 많은 팀들이 프론트엔드, 백엔드. 하이브리드 앱, 임베디드 장치 등 다양한 곳에서 자바스크립트를 사용하고 있습니다.

이 글은 자바스크립트의 동작 원리를 파헤치는 시리즈물의 첫 번째 입니다. 자바스크립트를 블록 단위로 구성하는 방법과 그 블록이 어떻게 동작 되어지는지 원리를 알면 더 좋은 코드와 앱을 개발할 수 있습니다.

GitHut stats 에서 볼 수 있듯, 자바스크립트는 Github 의 Active Repositories 와 Total Pushes 에서 1 위를 차지하고 있습니다. 다른 부분에서도 크게 뒤쳐져 있지 않습니다.

githutStats

(Check out up-to-date GitHub language stats).

많은 부분이 자바스크립트에 의존적인 프로젝트에서 개발자는 완성도 높은 소프트웨어를 개발하기 위해 자바스크립트의 언어적 특성과 내부 구조를 정확하게 이해해야 합니다.

알다시피, 많은 개발자들이 매일 자바스크립트를 사용하고 있지만 자바스크립트의 기본적인 동작 원리에 대해 모르는 경우가 많습니다.

개요

거의 대부분의 사람들이 V8 엔진에 대하여 들어봤을 것입니다. 또한, 자바스크립트가 싱글 쓰레드 기반이고 콜백 큐를 사용한다는 사실도 알고 있습니다.

이번 글에서는 우리는 이러한 개념들에 대해서 더 자세히 알아보고, 자바스크립트의 기본적인 동작 원리에 대해 알아볼 것입니다.

만약 당신이 자바스크립트에 친숙하지 않는다면, 자바스크립트가 다른언어와 비교하여 '특이한' 언어인지 깨닫게 될 것입니다.

자바스크립트가 익숙한 개발자라면, 매일 사용하는 자바스크립트 런타임에 대한 통찰력을 얻게 될 것입니다.

자바스크립트 엔진

자바스크립트 엔진의 대표적인 예로는 Google 의 V8 엔진이 있습니다. V8 엔진은 Chrome 과 Node.js 에서 사용되어집니다. 아래는 엔진의 구조도를 간단히 나타낸 그림입니다.

javascriptEngine

엔진은 크게 두 부분으로 구성됩니다.

  • Memory Heap: 메모리 할당이 이루어지는 곳
  • Call Stack: 코드가 실행되면서 스택 프레임이 쌓이는 곳

런타임

브라우저는 기본적으로 자바스크립트 개발자가 사용하는 많은 API 를 가지고 있습니다 (예시: "setTimeout"). 하지만, 이 API 를 자바스크립트 엔진이 제공하지는 않습니다.

그럼 이것들은 어디로부터 오는걸까요?

사실은 생각보다 더 복잡합니다.

runtime

자바스크립트 엔진 이외에도 자바스크립트에 관여하는 많은 요소들이 있습니다. DOM, Ajax, setTimeout 과 같이 브라우저가 제공해주는 API 를 Web API 라고 부릅니다.

또한, 이벤트 루프와 콜백 큐도 존재합니다.

콜스택

자바스크립트는 싱글 쓰레드 기반 프로그래밍 언어입니다. 다시말해, 호출스택이 하나라는 뜻입니다. 따라서 한 번에 하나의 작업만 처리할 수 있습니다.

콜스택은 기본적으로 우리가 프로그램 상에서 어디에 있는지를 기록하는 자료구조 입니다. 만약 실행 커서가 함수 안에 있을경우, 해당 함수는 호출 스택의 가장 윗부분에 위치하게 됩니다. 함수의 실행이 끝날 때, 해당 함수를 콜스택에서 제거합니다. 이것이 스택이 하는 일 입니다.

에를 들어보겠습니다, 아래 코드를 살펴보세요.

function multiply(x, y) {
  return x * y
}
function printSquare(x) {
  var s = multiply(x, x)
  console.log(s)
}
printSquare(5)

처음 엔진이 이 코드를 실행할 때 콜스택은 비어있는 상태입니다. 이후 코드가 실행되면서 콜스택은 아래와 같이 변합니다.

callstack1

콜스택의 각 단계를 스택 프레임(Stack Frame) 이라고 부릅니다.

또한 이것은 예외가 발생했을 때 스택 트레이스(Stack Trace)가 만들어지는 방식입니다. 스택 트레이스는 기본적으로 예외가 발생할 경우 콜스택의 상태입니다. 아래 코드를 살펴보세요,

function foo() {
  throw new Error('SessionStack will help you resolve crashes :)')
}
function bar() {
  foo()
}
function start() {
  bar()
}
start()

만약 이 코드를 크롬에서 실행하면 (이 파일이 foo.js 라는 파일에 들어있다고 가정할경우) 다음과 같은 스택 트레이스가 생성됩니다.

callstack2

콜스택이 최대 크기가 되면 "스택 날림(Blowing the stack)"이 일어납니다. 이런 현상은 반복문 코드를 광범위하게 테스트하지 않고 실행하였을 경우 종종 발생됩니다. 아래 코드를 살펴보세요,

function foo() {
  foo()
}
foo()

엔진에서 이 코드를 실행할 때, foo 함수가 먼저 실행됩니다. 하지만, foo 함수는 반복적으로 자신을 재호출하는 재귀함수 입니다. 따라서, 함수가 실행될 때마다 콜스택에 foo()가 쌓이게 됩니다. 아래 그림을 참고하세요,

callstack3

그러다 특정 시점에 함수 호출 횟수가 콜스택의 최대 허용치를 넘게 되면 브라우저는 다음과 같은 에러를 반환합니다.

callstack4

싱글 쓰레드 기반으로 코딩을 하는것은 멀티 쓰레드 환경에서 제기되는 복잡한 문제를 고민하지 않아도 되기 때문에 쉽다고 느껴질 수 있습니다. 얘를들어, 데드락 같은.

하지만 싱글 쓰레드에서 코드를 실행하는 것은 많은 제약이 따릅니다. 자바스크립트는 하나의 콜스택을 가지고 있습니다. 특정 코드의 실행이 느려지면 어떤일이 벌어질까요?

동시성 & 이벤트 루프

만약 콜스택 내에 처리 시간이 긴 함수가 있으면 무슨일이 발생할까요? 예를들어, 브라우저의 자바스크립트로 복잡한 이미지 변형 작업을 한다고 합시다.

이게 무슨 문제이지?라는 의문이 생길수도 있습니다. 여기에서 문제는 해당 함수가 실행되는 동안 브라우저는 아무 작업을 하지 못하고 대기 상태가 되어집니다. 브라우저는 페이지를 생성하지도 못하고, 아무 코드도 실행시키지 않은 상태로 대기하는 것입니다. 만약 자연스러운 UI 를 만들고 싶을경우 이는 문제가 될 수 있습니다.

다른 문제도 있습니다. 브라우저는 콜스택의 많은 작업을 수행하면서 오랫동안 응답을 하지 않을 수 있습니다. 이 경우에 대부분의 브라우저는 아래와같은 에러팝업을 띄우면서 해당 페이지를 종료할지 물어보게 됩니다.

concurrencyAndTheEventLoop1

이것은 이상적인 사용자 경험이 아닙니다.

그렇다면 페이지 렌더링 동작을 방해하지 않고 브라우저의 응답을 끊지 않으면서 연산량이 많은 코드를 실행하려면 어떻게 해야할까요? 정답은 바로 비동기 콜백 입니다.