자바스크립트 코드의 실행 과정 - 실행 컨텍스트

박민우·2023년 6월 15일
4

JavaScript

목록 보기
1/14
post-thumbnail

자바스크립트 코드,,? 그냥 한 줄씩 실행되는거 아냐,,,? 라고 생각할 수도 있겠지만 실제 자바스크립트 코드는 굉장히 복잡한 과정을 거쳐서 실행됩니다.

"자바스크립트에서 코드가 어떤 과정을 거쳐서 실행되나요?" 라고 물으면

=> 자바스크립트 엔진이 실행 컨텍스트를 기반으로 코드를 실행합니다! 라고 말할 수 있습니다.

이 간단한 문장을 좀 더 자세히 이해하기 위해서는 다음과 같은 내용들을 알아보겠습니다.

  1. 자바스크립트 엔진의 메모리 구조
  2. Call Stack VS Heap
  3. 실행 컨텍스트

📌 자바스크립트 엔진의 메모리 구조

자동차도 앞으로 달리기 위해서 엔진이 필요하듯, 자바스크립트 코드를 실행하기 위해서도 이를 실행해주는 엔진이 필요합니다.

자바스크립트 엔진이란 자바스크립트 코드를 실행하는 프로그램 혹은 인터프리터를 말한다. 자바스크립트 코드를 마이크로프로세서가 이해할 수 있도록 더 낮은 수준의 언어(기계어)로 변환해준다.

그 중, V8 엔진은 구글이 개발한 오픈소스로 가장 대중적인 자바스크립트 엔진이다. 우리가 자주 사용하는 Chrome 브라우저에 V8 엔진이 탑재되어있다.

자바스크립트 V8 엔진의 메모리 구조는 다음과 같다.

실제로는 이렇게 복잡하게 되어있지만,,,, 핵심만 보면

이렇게 크게 Memory HeapCall Stack으로 구성되어 있다. 자바스크립트는 단일 스레드(single thread) 프로그래밍 언어로, 자바스크립트 엔진에 Call Stack이 하나만 존재한다. 따라서, 멀티가 되지 않고 하나씩 하나씩 처리한다.

하지만 자바스크립트는 이벤트 루프를 통해 동시성을 지원할 수 있다! 자바스크립트 엔진은 해당 메모리 영역들을 어떻게 사용하며 코드의 흐름을 관리하는지 알아보자.


📌 Call Stack VS Memory Heap

Call Stack

  • 원시 타입 값과 실행 컨텍스트가 저장된다.

Memory Heap

  • Memory Heap에는 참조 타입(객체, 배열, 함수 등)이 저장된다.
  • Heap은 동적으로 데이터를 할당할 수 있는 메모리 영역이므로, 크기를 예측하기 힘든 참조 타입을 저장하기에 적합한 구조이다.

=> 자바스크립트 엔진의 메모리 구조 중 Call Stack이라는 곳에서 실행 컨텍스트가 저장되고, 관리되면서 코드의 실행이 이루어지는 것이다. 그럼 이 실행 컨텍스트란 무엇인지 자세히 알아보자.


📌 실행 컨텍스트(Execute Context)

실행 컨텍스트란?

실행 컨텍스트는 자바스크립트 코드가 실행되는 환경이다. 즉, 모든 JavaScript 코드는 실행 컨텍스트 내부에서 실행된다고 생각하면 된다.

자바스크립트 코드가 실행 컨텍스트를 기준으로 실행된다고 했는데,,, 😗

실행 컨텍스트는 자바스크립트 코드가 실행되는 환경이라 그러고,,, 😭

닭이 먼저냐 계란이 먼저냐는 말이 떠오르는 상황이지만, 조금만 더 자세히 알아보자🧐

함수가 실행되면 함수 실행에 해당하는 실행 컨텍스트가 생성되고, 자바스크립트 엔진의 Call Stack에 차곡차곡 쌓인다. 그리고 가장 위에 쌓여있는 컨텍스트에 해당하는 코드를 실행하면서, 전체 코드의 환경과 순서를 보장하게 된다.


실행 컨텍스트의 종류

자바스크립트에는 총 3가지 종류의 실행 컨텍스트가 있다.

  • 전역 실행 컨텍스트
    실행 컨텍스트의 기본이자 기초로, 함수 밖에 있는 코드는 전역 실행 컨텍스트에 있다. 프로그램에는 오직 한 개의 전역 실행 컨텍스트만 있을 수 있다. 브라우저의 경우 window 객체가 글로벌 객체이자 곧 전역 실행 컨텍스트가 된다.

  • 함수 실행 컨텍스트
    함수가 실행될 때마다, 해당 함수에 대한 완전 새로운 실행 컨텍스트가 만들어진다. 각 함수는 고유의 실행 컨텍스트를 갖지만, 함수가 실행되거나 call 될 때만 생성된다. 함수 실행 컨텍스트의 수는 제한이 없다. 새로운 실행 컨텍스트가 생성될 때마다, 정의된 순서에 따라 일련의 단계를 거친다.

  • Eval 실행 컨텍스트
    eval 함수 내부에서 실행되는 코드도 고유의 실행 컨텍스트를 갖고 있다. 하지만, eval이 자바스크립트 개발자들에게 쓰이는 일이 잘 없는 관계로, 여기서는 논의하지 않을 것이다.

=> 이렇게 코드 실행 시 생성되는 모든 실행 컨텍스트는 Call Stack이라고도 불리는 실행 스택(Execute Stack)에 저장된다.


Call Stack에서의 실행 컨텍스트 처리 과정

그러면 이러한 실행 컨텍스트는 언제 그리고 어떻게 생성되는 걸까 ?

  1. 엔진이 스크립트 실행 시작 시, 전역 실행 컨텍스트(Global Execute Context)를 생성 후 실행 스택에 push 한다.
  2. 이후 엔진이 함수 호출을 발견 할 때마다, 함수를 위한 새로운 함수 실행 컨텍스트를 생성하고 실행 스택에 push 한다.
  3. 엔진은 스택 최상단에 있는 함수의 실행 컨텍스트를 실행한다. 이 함수의 실행이 끝나면 해당 함수의 실행 스택이 현 스택에서 pop 되고 제어는 바로 그 아래 컨텍스트에 도달한다.

=> 여기서 중요한 것은, 함수 실행 컨텍스트는 함수가 실행될 때 만들어진다는 점이다. 자동으로 생성되는 전역공간과 eval을 제외하면, 실행 컨텍스트가 생성되는 시점은 곧 함수를 실행하는 시점인 것이다.

예시 코드와 이에 따른 콜 스택에서의 실행 컨텍스트 처리 과정을 그림으로 살펴보자 !

그러면 script 실행 시, 처음에 전역 실행 컨텍스트가 생성되고, 이후 함수가 호출될 때마다 함수 실행 컨텍스트가 생성되는 건 알겠는데, 그렇다면 각 실행 컨텍스트는 1. 어떤 구조2. 어떤 과정을 통해 생성될까 ?


1. 실행 컨텍스트의 구조

실행 컨텍스트는 크게 VariableEnvironment, LexicalEnvironment, ThisBinding 컴포넌트로 구성된다. 그림으로 보면, 다음과 같다.

Variable Environment

현재 컨텍스트 내의 식별자들에 대한 정보(Environment Record (snapshot)) + 외부 환경 정보(Outer Environment Reference (snapshot))를 담는다.

선언 시점의 Lexical Environment의 스냅샷이므로 변경 사항은 반영되지 않는다. 실행 컨텍스트를 생성할 때 Variable Environment에 정보를 먼저 담은 다음, 이를 복사해서 Lexical Environment를 만든다.

ES6 에서 Lexical EnvironmentVariable Environment의 차이점

  • Lexical Environment : 함수 선언과 변수(letconst)의 바인딩을 저장
  • Variable Environment : 변수 var 만 저장

Lexical Environment

렉시컬 환경은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록한다. 즉, identifier-variable mapping이 이루어지는 곳으로, 변수와 해당 변수에 대입된 값이 매핑되는 곳이라고 할 수 있다.

실행 스택이 코드의 실행 순서를 관리한다면 렉시컬 환경은 스코프와 식별자를 관리한다. 렉시컬 환경은 키와 값을 갖는 객체 형태의 스코프를 생성해서, 식별자를 키로 등록하고 식별자에 바인딩된 값을 관리한다.

처음에는 Variable Environment와 같은 값을 가지지만 변경 사항이 실시간으로 반영된다.

  • Environment Record (환경 레코드)

    함수 안의 코드가 실행되기 전에 현재 컨텍스트와 관련된 코드의 식별자 정보가 저장된다. 즉, 실행 컨텍스트 내의 변수와 함수 선언이 저장되는 곳으로, 매개변수 식별자, 선언된 함수 등이 해당된다.

    즉, 코드가 실행되기 전에 엔진은 실행 컨텍스트를 생성해 Environment Record에 변수를 저장하기 때문에, 엔진은 이미 해당 환경에 속한 코드의 변수명 등을 모두 알고 있게 된다. => 호이스팅

  • Outer Environment Reference (외부 환경에 대한 참조)

    호출된 함수가 선언될 당시의 렉시컬 환경을 참조하는 포인터로, 스코프 체인을 가능하게 한다. 자바스크립트 엔진이 현재 렉시컬 환경에서 변수를 찾을 수 없다면 외부 환경에서 변수를 찾는 것이다. 만약 상위 스코프에서도 해당 식별자를 찾을 수 없다면 참조 에러(uncaught reference error)를 발생시킨다.

    💡스코프 체인

    자바스크립트는 변수의 유효 범위를 검색할 때 안 => 밖 으로 찾아나가는데, 이것을 스코프 체인(scope chain)이라고 한다.

    따라서 전역 스코프에 선언된 변수들은 어느 곳에서도 접근이 가능하다. 반면 지역 스코프는 선언된 함수의 내부에서만 접근이 가능하다.

    Outer Environment Reference는 현재 호출된 함수가 선언되는 시점에서의 Lexical Environment를 참조하는 포인터이다. Outer Environment Reference는 연결리스트 형태로, 선언 시점의 Lexical Environment를 계속 찾아 올라간다. 선언 시점의 Lexical Environment는 결국 해당 함수가 속한 상위 스코프의 범위이다. 각 함수의 Outer Environment Reference는 오직 자신이 선언된 시점의 Lexical Environment만 참조하고 있기 때문에 가장 가까운 요소부터 위로 차례대로만 접근할 수 있다.

=> Variable EnvironmentLexical Environment의 구조가 동일함에서 대충 알 수 있듯이, 사실 두 컴포넌트는 모두 Lexical Environment이다(?). 두 컴포넌트 모두 스코프와 식별자를 관리하는 역할을 한다. 하지만 ES6에서는 이를 Variable EnvironmentLexical Environment로 구분하고 있고, 이에 대한 이유는 다른 글에서 다루도록 하겠다 !


This Binding

this 식별자가 바라봐야 할 대상 객체를 의미한다. 실행 컨텍스트가 활성될 때 this가 지정되지 않은 경우 this에는 전역 객체가 저장된다.


2. 실행 컨텍스트 생성 과정

위와 같은 구조를 가지는 실행 컨텍스트는 1) Creation Phase2) Execution Phase 단계를 거쳐서 생성된다.

1) Creation Phase

여기서는 LexicalEnvironment, VariableEnvironment, ThisBinding 컴포넌트를 생성한다.


실행 컨텍스트는 개념적으로 다음과 같이 표현된다.

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref . to VariableEnvironment in memory>,
}

Lexical Environment

var a = 20;
var b = 40;

function foo(){
	console.log('bar');
}

다음과 같은 코드에서 LexicalEnvironment는 아래와 같이 표현된다.

lexicalEnvironment = {
  a : 20,
  b : 40,
  foo : <ref. to foo function>
}

2) Execution Phase

Creation Phase를 통해 Scope Chain, 변수, 함수, 인자들을 만들고, this를 결정한 후, Execution Phase에서는 자바스크립트 엔진이 한 줄 한 줄 위에서부터 코드를 읽으면서 코드를 실행한다.

이 단계에서 가장 중요한 것은 이 때, 선언했던 변수들에 값이 할당된다는 것이다.


➕ 호이스팅

위에서 Execution ContextLexical EnvironmentVariable Environment를 가진다고 했고, 바로 이 때문에 변수 선언 방식 let, const와 var의 호이스팅이 서로 차이가 나는 것이다.

변수 선언 시의 과정을 간략히 나타내보면,

let variable = 126;

위와 같은 코드가 실행될 때

  1. 변수의 고유식별자(변수명)을 생성하고
  2. 메모리에 주소를 할당하고
  3. 생성한 주소에 을 넣는다.

이렇게 3가지 단계로 나누어 실행된다.


이를 실제 실행 컨텍스트 관점에서 다시 표현해보면 아래와 같다.

1. Declaration phase (선언 단계)

=> 변수를 Execution Context의 Lexical Environment에 등록.

2. Initialization phase (초기화 단계)

=> Lexical Environment에 등록되어 있는 변수를 위하여 메모리를 할당하는 단계. 여기서 변수는 undefined로 초기화된다.

3. Assignment phase (할당 단계)

=> 변수에 실제로 값이 할당되는 단계이다. (undefined → 특정한 값)

여기서의 Lexical EnvironmentLexical EnvironmentVariable Environment를 총칭하며, var로 선언된 변수는 Variable Environment에 매핑되고, 함수와 let, const는 Lexical Environment에 매핑된다.


let, const와 var은 이 3단계 과정을 거치는 부분에서 차이가 발생하는데, var의 경우 1단계와 2단계가 모두 진행되는데 let, const는 1단계만 먼저 진행된다.

즉, Execution Context의 생성 과정 중 첫 번째 페이즈인 Creation Phase에서

Variable Environment에서는 var로 선언된 변수를 변수 선언 1, 2단계(Declaration phase, Initialization phase) 모두 진행하기에 메모리에 매핑되고 undefined로 초기화까지 마치게 된다.

반면에 Lexical Environment의 경우는 let, const로 선언된 변수를 1단계(Declaration phase)만 진행하기에 Variable Environment와는 동작 방식에 차이가 있다.


📌 정리 및 예시

위 내용을 모두 정리하면,

javascript 코드는

  1. 자바스크립트 엔진이
  2. Call Stack이라는 메모리 영역에서
  3. Lexical Environment 컴포넌트를 가지는 실행 컨텍스트를 Create하고, Execute하는 과정을 거쳐 실행된다.

라고 말할 수 있겠다.


지금까지 배운 내용을 여기 슬라이드에 나와있는 V8 엔진의 JS 코드 실행과정을 보며 정리해보자. 위 슬라이드를 통해

  • 글로벌 스코프는 스택의 Global frame에 저장되고, 이는 전역 실행 컨텍스트를 의미한다.
  • 함수를 호출할 때마다 해당 함수 실행 컨텍스트가 프레임 블록으로 스택에 추가되고, 함수가 종료(리턴)되면 스택에서 제거된다. 그리고 함수 인자, 함수 내의 지역 변수 및 리턴값은 이 프레임 블록에 저장된다.
  • String, Number와 같은 원시 타입은 콜 스택(실행 컨텍스트 내 렉시컬 환경)에 바로 저장되고, 모든 객체 타입은 힙에 생성되고, 스택 포인터를 통해 스택에서 힙을 참조한다.
  • 메인 프로세스의 실행이 완료되면 스택에서 힙에 있는 객체를 참조하지 않으므로 힙에 남아있는 객체들은 고아(orphan)가 된다.

등의 내용을 알 수 있다.


📌 참고 및 이미지 출처

참고

실행 컨텍스트란 무엇인가요?

자바스크립트의 실행 컨텍스트

[JS] 자바스크립트의 The Execution Context (실행 컨텍스트) 와 Hoisting (호이스팅)

모던 자바스크립트에서 호이스팅(Hoisting)

ES6의 Execution Context(실행 컨텍스트)의 동작 방식과 Lexical Nesting Structure(Scope chain)

V8 엔진의 JS 코드 실행과정

이미지 출처

자바스크립트 엔진 메모리 구조 사진 출처

Call Stack & Memory Heap 사진 출처

실행 스택에서의 실행 컨텍스트 사진 출처

실행 컨텍스트 구조 사진 출처

profile
꾸준히, 깊게

1개의 댓글

comment-user-thumbnail
2023년 6월 16일

Creation Phase부터 어질어질하네요.. 정리 감사합니다😊

답글 달기