[javascript] 자바스크립트 엔진의 구조 v8

유재민·2022년 1월 15일
0

# 자바스크립트 엔진의 구조

아래의 이미지는 자바스크립트 V8 엔진의 구조이다.


# 콜스택(Call Stack)과 메모리 힙(Memory Heap)

  • 콜스택 : 실행 컨텍스트를 저장하는 자료 구조이다. 원시 타입의 데이터가 저장되며, 실행 컨텍스트를 통해 변수 식별자(이름) 저장, 스코프 체인 및 this 관리, 코드 실행 순서 관리 등을 수행한다.

  • 메모리힙 : 참조타입(객체 등) 데이터가 저장되며 구조적이지 않다.


# 콜스택과 메모리 힙의 데이터 저장/참조 원리

위의 이미지와 같이 주소와 값 형태로 이루어져 있으며 변수 a에 데이터를 할당 시에
변수는 그 데이터를 가진 주소를 가리키게 된다.
또한 변수 b와 값이 데이터 타입이 원시 타입이 아닌 경우,
원시 타입의 값은 그 데이터를 가진 메모리 힙에 주소를 가리키게 되는 것이다.

변수 식별자 a 자체는 콜스택 상의 실행컨텍스트(Execution Context)의 렉시컬 환경(Lexical Environment)이라는 곳에 저장된다.

변수 a에 20을 재할당하면, 본인의 메모리에 있는 값을 변경하는게 아니라, 기존에 20을 저장하고 있는 메모리의 주소값으로 교체한다.
a에 저장된 주소값은 20을 가리키고 있던 b에 저장된 주소값과 동일해진다.
이 때 a가 처음에 가지고 있던 10을 가진 메모리 주소는 쓸모없는 값이 되므로
가비지 컬렉터가 이러한 데이터를 적절한 시점에 제거하게 된다.
만약 여기서 b에 값을 30으로 재할당하면, 30이라는 값을 가진 메모리 공간을 새로 생성 후 b는 그 메모리 공간의 주소를 가리키게 된다.
또한 const의 경우 위와 같은 방식으로 동작이 불가능하다. const는 초기값 할당이 필수이며 주소값을 바꾸는 것이 불가능하기 때문이다.

또한 위에 개념에서 const에서 push와 같은 메소드가 동작하는 이유를 알 수 있다. 메모리힙에 저장된 배열에 값이 추가되는 것일 뿐, 변수가 가리키는 주소가 변하는 것이 아니기 때문이다.


# 실행컨텍스트(Execution Context)

실행 컨텍스트란 실행 코드에 제공할 정보들을 모아 놓은 객체이다.
콜스택에 쌓이며 전역 컨텍스트는 자동으로 먼저 생성되고, 함수 호출 시 함수 컨텍스트가 생성된다.

# 실행컨텍스트의 구조

실제 사용 되는 정보는 렉시컬 환경(LexicalEnvironment)의 환경 레코드(environmentRecord)와 아우터(outerEnvironmentReference)이다. 환경 레코드는 식별자를 저장하며, 아우터는 실행컨텍스트를 잇는 연결다리이다. 환경 레코드의 특징에서 호이스팅이 발생하는 것을 알 수 있으며 아우터의 특징에서 스코프 체인이 발생하는 것을 알 수 있다.

# 실행컨텍스트의 정보를 객체로 표현

실행 컨텍스트 (Execution Context) : {
  동적인 환경 (Variable Environment) : {…},
  어휘적 환경 (Lexica lEnviroment) : {
    환경 레코드(Environment Record): {
      객체적 환경 레코드(Object Environment Record),
      선언적 환경 레코드(Declarative Environment Record): {
        함수 환경 레코드(Function Environment Record),
        모듈 환경 레코드(Module Environment Record)
      },
      디스 바인딩(this binding)
    },
    외부 환경에 대한 참조(Reference to the outer environment)
  }
}
  • 동적인 환경 (Variable Environment) : 변수(var) 바인딩만 저장
  • 어휘적 환경 (Lexical Enviroment) : 함수 선언 및 변수(let and const) 바인딩을 저장
  • 환경 레코드(Environment Record) :  Lexical 환경 안에 변수와 함수의 선언이 저장되는 공간, 함수컨텍스트의 경우 인수 포함
  • 객체적 환경 레코드(Object Environment Record) : 변수와 함수 선언과는 별도로 객체 환경 레코드에는 전역 바인딩 객체(브라우저의 경우 window 객체)도 저장. 따라서 바인딩 오브젝트의 각 속성(브라우저의 경우 브라우저가 window 객체에 제공하는 property들과 method를 포함)에 대해, 레코드에 새로운 엔트리가 생성.
  • 선언적 환경 레코드(Declarative Environment Record) : 변수와 함수 선언도 저장, 함수 코드의 lexical 환경은 선언 환경 레코드를 포함
  • 함수 환경 레코드(Function Environment Record) : 함수의 최상위 스코프를 나타내는데 사용되는 선언적 환경 레코드. 화살표 함수가 아니라면 this 바인딩을 제공하며 화살표 함수가 아니고 super를 참조하는 경우 super 메서드를 실행하는데 필요한 state를 제공
  • 모듈 환경 레코드(Module Environment Record) : Module의 외부 스코프를 나타낼 때 사용하는 선언적 환경 레코드. 변경 가능, 변경 불가능한 바인딩과 더불어 변경 불가능한 import 바인딩(immutable import binding)을 제공
  • this바인딩(this binding) : this의 값이 결정되거나 설정
  • 외부 환경에 대한 참조(Reference to the outer environment) : 스코프 체인 결정

# 실행컨텍스트의 생성과 실행

전역 컨텍스트는 자바스크립트 실행과 동시에 자동으로 생성되며 함수 컨텍스트는
함수를 호출할 때 생성된다. 또한 실행 컨텍스트는 생성단계와 실행단계로 나누어 생성된다.

(1) 생성(평가) 단계(Creation Phase)
생성단계에서 렉시컬 환경 컴포넌트와 변수 환경 컴포넌트를 생성한다. 렉시컬 환경 컴포넌트는 3가지 일을 하는데 첫번째로 환경 레코드에 식별자 정보를 저장하고 두번째로 외부 렉시컬 환경을 참조하여 스코프 체인을 형성하며 마지막으로 this에 바인딩 될 값을 결정한다.

(2) 실행 단계(Execution Phase)
자바스크립트 엔진이 한줄 한줄 위에서 부터 코드를 읽으면서 코드를 실행하는 단계이다.
이 단계에서 중요한 점은 선언했던 변수들에 값이 할당된다는 것이다.


# 실행컨텍스트의 실행 순서 예시

var a = 1;

function fn2(){
    console.log('f2')
}
function fn1(){
    console.log('f1')
    fn2();
}
fn1();
console.log(a);

var b = 2;

function fn3(){
    console.log('f3')
    const f4='f4';
    function fn4(){
        console.log(f4)
    }
    fn4();
}

fn3();
console.log(b);
  1. 자바스크립트가 실행되면 전역 코드 평가 단계에서 콜스택에 전역컨텍스트를 생성하고 환경레코드에 fn2,fn1,fn3,a,b의 식별자 정보를 등록한다. 또한 var변수의 경우 선언단계와 초기화단계가 함께 이루어지기때문에 등록 후 undefined로 초기화되며 함수는 바로 function으로 초기화한다.
  2. 전역 코드 평가단계가 끝난 뒤 전역 코드 실행단계로 들어간다. 전역 코드 실행단계에서 한줄씩 읽어내려가는데 일단 첫번째 변수인 a변수를 만나게 되면 undefined의 값을 1로 할당한다. 그 후 fn1()호출문을 만나게 되면 전역코드실행이 중단되고 제어권이 fn1함수 내부로 이동한다. 그 후 fn1함수의 함수 평가 단계를 거친다. 콜스택에 Fn1함수컨텍스트를 생성하고 환경레코드에 fn2 식별자 정보를 등록한다. 그 후 외부 환경레코드를 참조하여 스코프체인을 결정하고 this를 바인딩한다. 이 때 this를 따로 지정해주지 않으면 전역객체인 window에 바인딩된다. 평가단계가 끝난 뒤 코드 실행단계가 되고 한줄씩 코드를 읽어가며 실행한다. 이 때 console.log(‘f1)이 실행되어 콘솔에 f1이 찍힌 뒤 fn2()호출문을 만나 fn1함수의 실행을 멈추고 제어권이 fn2함수로 넘어간다. fn2함수의 함수 평가 단계에서 fn2 함수컨텍스트를 생성하고 식별자정보 등록, 스코프체인, this바인딩의 단계가 끝난 뒤 실행단계로 넘어가 console.log(‘f2)를 실행하여 콘솔에 f2가 찍힌다. 함수가 종료되었으므로 제어권은 다시 fn1함수로 넘어오고 fn2함수는 실행이 완료되었으므로 콜스택에서 제거된다. 그 후 f1도 실행이 완료되었으므로 실행컨텍스트에서 삭제된다. (콜스택은 전역컨텍스트를 제외하고 비어있게 된다.)
  3. 전역 코드 실행단계로 돌아간다. b에 2를 할당하고 fn3호출문을 만났을 때 다시 제어권은 fn3함수내부로 이동된다. 이 때 함수평가단계를 거쳐 함수컨텍스트를 콜스택에 담는다. 그 후 실행단계에서 한줄씩 실행하게되며 console.log(‘f3’)이 실행되고 f3이 콘솔에 찍히게 된다. 그 후 f4변수에 ‘f4’라는 값이 할당되고 fn4호출문을 만나 제어권이 fn4함수 내부로 이동하게 된다. 그 후 또다시 함수 평가단계를 거치고 콜스택에 실행컨텍스트를 쌓은 뒤 실행단계에서 console.log(‘f4’)가 실행되어 콘솔에 f4가 찍히게 된다. 찍히는 이유는 외부 환경레코드를 참조하는 스코프체인을 통해 f4의 값을 가져올 수 있었다. 이처럼 스코프의 결정은 함수가 선언된 위치에 따라 결정된다. 그 후 함수가 종료되면 제어권이 다시 fn3으로 넘어오고 f4함수는 실행이 끝났으므로 f4실행컨텍스트가 콜스택에서 삭제된다. 그 후 f3함수의 실행이 종료되면 제어권이 다시 전역 코드 실행단계로 돌아가며 fn3함수는 실행이 끝났으므로 콜스택에서 실행컨텍스트가 삭제된다.
  4. 그 후 console.log(b)가 실행되며 2가 콘솔에 찍힌다.
  5. 만약 Web API에서 제공하는 비동기 처리(setInterval, setTimeout 등)가 존재한다면 무조건 마지막으로 호출된다. 비동기 처리 함수 또한 다른 함수들과 마찬가지로 생성단계를 거쳐 실행단계를 지나면 다른 실행컨텍스트와 마찬가지로 콜스택에서 제거된다. 이 때 Web API에 타이머를 보내고 Web API는 타이머를 재기 시작한다. 타이머가 완료되면 웹 API는 setInterval()이 가지고 있던 콜백 함수를 테스크 큐에 밀어 넣는다. (선입선출방식) 테스크 큐는 콜스택이 비어있는 것을 보고 테스크 큐에 있는 콜백 함수를 콜스택에 전달하고 콜스택은 함수를 실행한다. 즉, 전역코드실행단계까지 완료된 후 전역컨텍스트까지 콜스택에서 삭제된 후에야 실행되기때문에 가장 마지막으로 실행됨을 알 수 있다.
  • 콜스택의 후입선출방식, 테스트큐의 선입선출방식, 싱글스레드언어(한번에하나의테스크실행), 스코프체인의 결정, this바인딩, 호이스팅의 원리를 알 수 있다.
  • js의 이벤트리스너 또한 똑같다. 함수 내부에 이벤트 리스너가 있는 경우 함수가 호출문을 만나면 실행컨텍스트에 쌓였다가 실행컨텍스트가 삭제되며 web API에 보관된다. 이벤트 동작을 실행했을 때 테스크큐로 옮겨지고 이벤트루프를 통해 콜스택이 비어있는지 확인 후 콜스택에 밀어내어 콜백함수를 실행하게 된다.
  • 스코프 체인은 호출이 아닌 선언 시에 이미 결정된다.
  • 이벤트 루프는 콜스택과 테스크큐를 감시하여 콜스택이 비어있을 경우 테스크큐에 있는 콜백 함수를 콜스택에 밀어넣는 역할을 한다. 이벤트 루프는 자바스크립트 엔진이 아닌 구동환경(브라우저,노드)가 가지고 있는 장치이다.
  • 태스크 큐는 javascript 실행환경인 브라우저에 위치하며 태스크 web api를 생성하는 경우에 생성된다.
  • 웹 api는 javascript 쓰레드가 아닌 별도의 쓰레드를 가진다. 이 별도의 쓰레드에서 특정 상황에 콜백 함수를 테스크 큐에 넘기는 것이다.
  • 실행컨텍스트가 보유한 프로퍼티는 Lexical Environment, Variable Environment가 있는데 실제 사용되는 정보가 담긴 곳은 Lexical Environment이다.
  • if, for문과 같이 블록레벨스코프의 경우 EC는 생성하지 않은 채 상위 EC를 그대로 쓰며, block scope만의 LE를 별도로 생성하여 상위 EC가 기존의 LE 대신 새로 생성한 LE를 바라보도록 했다가, block scope가 종료된 시점에 원래의 LE를 복구하는 방식이다.

# 비동기 처리(Web API, Queue, Event Loop)

자바스크립트는 싱글 스레드 언어이다. 동기적 요청을 통해 코드를 한줄 한줄 처리한다.
하지만 이러한 특성으로 인해 콜스택에 실행컨텍스트가 남아있는 동안 브라우저는
아무것도 할 수가 없다. 이러한 문제를 비동기 처리로 해결할 수 있다.

Web API에서 제공하는 setInterval()의 경우 생성단계를 거쳐 실행단계를 지나면
다른 실행컨텍스트와 마찬가지로 콜스택에서 제거된다. 이 때 웹 API에 타이머를 보내고
Web API는 타이머를 재기 시작한다. 타이머가 완료되면 웹 API는 setInterval()이 가지고 있던 콜백 함수를 테스크 큐에 밀어 넣는다. 테스크 큐는 콜스택이 비어있는 것을 보고 테스크 큐에 있는 콜백 함수를 콜스택에 전달하고 콜스택은 함수를 실행한다.

이런 예시를 활용하여 setInterval()의 타이머를 0으로 설정하면 콜스택이
비어있을 때까지 기다렸다가 실행한다는 것을 알 수 있다.

  • Web API : 웹 브라우저에서 제공하는 API로 AJAX나 Timeout등의 비동기 작업을 실행
  • Task Queue : Callback Queue라고도 하며 Web API에서 넘겨받은 Callback함수를 저장(선입선출 방식)
  • Event Loop : Call Stack이 비어있다면 Task Queue의 작업을 Call Stack으로 옮김


참고자료

profile
프론트엔드 개발자

0개의 댓글