logo
Javascript 동작 원리 (2) - Execution Context

Javascript 동작 원리 (2) - Execution Context

Execution Context를 한 줄 요약을 하면 아래와 같다.

"현재 실행 중인 코드가 참조할 수 있는 정보(변수, 함수, this, 스코프 등)를 한 묶음으로 관리하는 실행 단위"

JavaScript는 C/C++처럼 main() 함수 안에서만 코드가 실행되는 구조가 아니다. 전역(Global)에서도 코드를 작성할 수 있고, 함수 내부에서도 별도의 코드 블록이 실행된다.

엔진 입장에선 “지금 어떤 범위의 코드가 실행 중인지”를 추적해야 하므로, 실행 단위별로 컨텍스트를 만들어 관리한다.

Execution Context

Execution Context는 크게 두 종류로 나뉜다.

  • Global Execution Context: 파일(스크립트)의 전역 코드가 실행될 때 생성되는 컨텍스트
  • Function Execution Context: 함수가 호출될 때마다 생성되는 컨텍스트

즉, 실행이 시작되면 Global Execution Context가 먼저 생성되고, 함수가 호출될 때마다 그 위에 Function Execution Context가 추가로 생긴다.

Execution Call Stack

C/C++에서는 main()이 스택의 시작점이지만, JavaScript는 명시적인 entry 함수가 없기 때문에 Global Context가 스택의 바닥에 깔린다. 이후 함수 호출이 일어나면 컨텍스트가 스택 위로 쌓이고, 함수 실행이 끝나면 스택에서 빠진다.

이 방식 덕분에 아래가 자연스럽게 가능해진다.

  • 함수가 함수 안에서 다시 호출되는 구조
  • 재귀 호출
  • 호출이 끝나도 “위쪽 컨텍스트가 유지된 상태에서” 다음 컨텍스트가 쌓이는 흐름

Execution Context Phases

그렇다면 Execution Context의 주기는 어떻게 될까?

Execution Context의 주기는 일반적으로 아래와 같이 생성 단계와 실행 단계로 나눈다.

  1. The Creation Phase (생성 단계)
  2. The Execution Phase (실행 단계)

1) Creation Phase (생성 단계)

생성 단계에서 엔진은 코드를 실행하기 전에 "실행을 위한 준비"를 한다. 이때 처리되는 항목은 다음과 같다.

  • this 값 결정
  • 전역환경인 경우 Global Object(전역 객체)와 연결 설정
  • 함수코드인 경우 arguments 객체 설정
  • 변수/함수 선언문을 모두 스캔해서 메모리 공간 확보

Creation Phase(생성 단계)의 특징

Execution Context의 생성단계에서 변수선언문과 함수선언문 각각 처리되는 방식이 극명하게 다르다.

  • 특징 1: 변수선언문은 메모리만 할당받는다.
  • 특징 2: 함수선언문은 메모리 적재까지 이뤄진다.

var 키워드로 선언된 변수의 경우 메모리영역만 먼저 확보되고, 값이 할당되지 않음을 의미하는 undefined 로 초기화된다.

때문에 아래 코드가 실행되면 생성단계에서 할당받는 기본값인 undefined 가 출력된다.

console.log(a); // undefined
var a = 1;

그러나 function 키워드를 통한 함수선언문은 실질적인 메모리적재까지 받는다.

때문에 코드의 물리적인 순서와 관계없이 호출가능한 상태가 되고 이런 이질적인 현상을 JS의 Hoisting 현상이라고 부르기도 한다.

그런데 이러한 Hoisting현상은 Execution Context의 생성단계에 의한 단순한 성질일 뿐이었다. 

겉으로는 함수코드가 물리적으로 위쪽으로 이동된 게 아닐까 싶지만, 근본적인 이유는 실행 전에 함수코드만 메모리적재를 먼저 받아내기 때문에 그렇다.

2) Execution Phase (실행 단계)

생성단계를 실행에 필요한 준비단계라고 한다면, 실행단계는 코드의 직접적인 실행이 이뤄지는 단계라고 봐야한다.

생성 단계에서 확보해둔 변수들의 메모리영역에 값 대입도 당연히 여기서 일어난다.

var a = 1;

예로 위 코드는 실행 단계에서 a에 "number" 1이 할당된다. 생성단계에서는 메모리만 확보하여 undefined 였다면 실행단계에서는 비로서 a =1에 도달하며 원하는 값이 할당된다.

Closure: Execution Context와 맞닿는 지점

추가로, 앞서 다룬 Execution Context의 원리는 JS의 클로저(Closure)와도 밀접하게 연결된다.

클로저란, 함수가 자신이 생성될 때의 Lexical Environment(어휘적 환경)에 대한 참조를 유지해서, 해당 함수의 실행이 끝난 뒤에도 외부 스코프의 변수에 접근할 수 있는 특성을 말한다.

아래 예시를 보자.

function outerFunction() {
  let outerVar = 'I am from outerFunction';

  return function innerFunction() {
    console.log(outerVar); // 부모 함수의 변수를 참조
  };
}

const closure = outerFunction();
closure(); // "I am from outerFunction"

위 코드가 실행되면 총 세 개의 Execution Context가 다음 순서로 Call Stack에 쌓이고 제거된다.

1. Global Execution Context                                                                                               

2. outerFunction의 Execution Context

3. innerFunction의 Execution Context

여기서 주목할 점이 있다.

outerFunction()의 실행이 끝나고 Call Stack에서 제거된 이후에도, innerFunction은 outerFunction 안에 선언된 outerVar를 계속 참조할 수 있다.

이는 innerFunction이 생성시점의 부모 스코프에 대한 참조를, Execution Context 내부의 Lexical Environment를 통해 유지하고 있기 때문이다.

"Call Stack에서 이미 빠졌는데 왜 값이 살아 있는가?"라는 의문이 들 수 있다. 답은 간단하다.

  • 자식 함수가 부모 스코프의 Lexical Environment를 참조하고 있고,
  • JS 엔진은 아직 참조 중인 환경을 GC(가비지 컬렉션) 대상에서 제외한다.

이것이 클로저가 동작하는 원리다. 강력하고 편리하지만, 때로는 직관적으로 이해하기 어려운 이유이기도 하다.

+ 주의: 클로저는 불필요하게 큰 객체를 캡처하면 메모리 유지 비용이 생길 수도 있다.

출처

https://www.webdevlog.com/p/how-js-works-execution-context

https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0

Written on

2024-09-09 11:47

Comments