Javascript : 자바스크립트 엔진과 런타임

Javascript : 자바스크립트 엔진과 런타임

자바스크립트 엔진

구글의 V8은 Chrome과 Node.js에서 사용하는 자바스크립트 엔진의 대표적인 예이다.

자바스크립트 엔진 기초 구조

Memory Heap : 메모리 할당이 일어나는 곳

: 메모리 할당이 일어나는 곳 Call Stack : 코드 실행에 따라 호출 스택이 쌓이는 곳

호출 스택 (Call Stack)

자바스크립트는 단일 스레드 프로그래밍 언어이므로 단일 호출 스택이 있다. 따라서 한 번에 한 가지 작업만을 수행할 수 있다.

호출 스택에 쌓인 함수나 코드를 위에서부터 아래로 동기적으로 실행한다. 그리고 작업 중인 함수나 코드가 끝나면 pop하고 바로 아래의 함수나 코드를 실행한다. 작업을 차례대로 실행하므로 하나의 작업이 끝날 때까지 또 다른 작업을 실행하지 않는다.

다음의 코드 예시를 본다면

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

호출 스택의 순서는 다음과 같다.

콜 스택 흐름

호출 스택의 각 단계를 스택 프레임(Stack Frame)이라고 한다.

그리고 보통 예외가 발생했을 때 콘솔 로그 상에서 나타나는 스택 트레이스(Stack Trace)가 오류가 발생하기까지의 스택 트레이스들로 구성된다.

스택 트레이스는 에러가 발생하기 이전 호출되어 콜 스택에 쌓인 모든 함수들이다.

아래 코드도 살펴보면

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

스택 트레이스 예시

스택 날리기

최대 호출 스택 크기에 도달했을 때 발생하는데, 특히 재귀를 사용하는 경우에는 매우 쉽게 발생할 수 있다.

예시를 보자면

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

foo 함수가 재귀적으로 콜스택에 계속해서 쌓여나가는 경우 어느 시점에서 브라우저는 다음과 같은 오류를 발생시켜 조치를 취하게 된다.

무한 재귀로 인한 콜 스택 박살

자바스크립트 런타임

자바스크립트 엔진 밖에서도 자바스크립트에 관여하는 요소들이 있다(Wep API, Task Queue, Event Loop등).

런타임은 특정 언어로 만든 프로그램들을 실행할 수 있는 환경이다. Node.js나 크롬등의 브라우저들은 자바스크립트가 구동되는 환경이기 때문에, 이를 자바스크립트 런타임이라고 한다.

엔진과 런타임의 상호작용

동시성(Concurrency) & 이벤트 루프(Event Loop)

자바스크립트처럼 단일 스레드 환경에서 호출 스택에 처리 시간이 오래 걸리는 함수가 있으면 해당 함수가 실행되는 동안 브라우저는 아무 작업도 할 수 없게 된다. 즉, 브라우저는 페이지를 그리지도 못하고, 어느 코드도 실행을 못한다. 사용자가 답답해 돌아가실 지경일 것이다.

이 런타임이 자바스크립트의 비동기적 행위를 가능하게 해준다.

Task Queue와 Event Loop

자바스크립트에서 비동기로 호출되는 콜백 함수들은 호출 스택(Call Stack)에 쌓이지 않고 태스크 큐(Task Queue)로 보내진다.

조금 더 자세하게 얘기하면, setTimeout 같은 비동기 함수를 호출하게 되면 web API 이므로 브라우저에 위임한다. 그리고 비동기적 작업이 끝나게 되면 수행하는 callback 함수가 태스크 큐에 들어가게 된다.

태스크 큐는 말 그대로 콜백 함수들이 대기하는 큐(FIFO) 형태의 리스트라 할 수 있고, 이벤트 루프는 호출 스택이 비워질 때(isEmpty)마다 큐에서 콜백 함수를 꺼내와서 실행하는 역할을 해 준다.

태스크 큐는 콜백 함수가 담기는 곳이기도 하여 Callback Queue로 칭하기도 한다.

MDN의 이벤트 루프 설명을 보면 왜 '루프'라는 이름이 붙었는지를 아주 간단한 가상코드로 설명하고 있다.

while(queue.waitForMessage()){ queue.processNextMessage(); }

위 코드의 waitForMessage() 메소드는 현재 실행중인 태스크가 없을 때 다음 태스크가 큐에 추가될 때까지 대기하는 역할을 한다.

이런 식으로 이벤트 루프는 '현재 실행중인 태스크가 없는지'와 '태스크 큐에 태스크가 있는지'를 반복적으로 확인한다.

간단하게 정리하면 다음과 같다.

모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가한다.

이벤트 루프는 '현재 실행중인 태스크가 없을 때'(주로 호출 스택이 비워졌을 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다.

유명한 예시 코드를 보면

console.log(1); setTimeout(() => { console.log(2); }, 0); console.log(3);

위의 호출 순서는 1, 3, 2 순으로 출력이 된다.

이렇게 되는 과정을 간단하게 설명하자면

1. console.log(1) 함수가 콜 스택으로 들어가서 바로 호출한다.

2. setTimeout을 만나 브라우저 web API에 위임한다.

3. 바로 console.log(3) 함수가 콜 스택으로 들어가서 바로 호출한다.

이 때 0초임에도 불구하고 console.log(3)이 수행되는 이유는 setTimeout을 브라우저에 위임하자마자 바로 다음 라인으로 넘어가 console.log(3) 함수를 콜 스택에 담기 때문이다.

Reference

https://meetup.toast.com/posts/89

https://ingg.dev/js-work/#run

https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

from http://woogie.tistory.com/19 by ccl(A) rewrite - 2021-10-06 04:28:05