해당 포스팅은 위키북스의 모던 자바스크립트 Deep Dive라는 책을 독학하며 기록하는 글입니다.

실행 컨텍스트는 자바스크립트의 핵심 개념이다. 다소 이해하기 난해한 내용이니 설명이 좀 길어질 수도 있다.

자바스크립트에서의 소스코드

ECMAScript 사양에서는 소스코드를 다음 4가지 타입을 구분한다.

  1. 전역코드
  2. 함수 코드
  3. eval 코드
  4. 모듈 코드

위의 각 코드는 실행 컨텍스트를 생성하면 4가지 타입으로 구분하는 이유는 소스코드의 타입에 따라 실행 컨텍스트를 생성하는 과정과 관리 내용이 다르기 때문이다.

소스코드의 평가와 실행

자바스크립트엔진은 자바스크립트의 모든 코드를 평가와 실행 두 단계로 나누어 처리한다.

소스코드를 평가하면 실행 컨텍스트가 생성되며 실행 컨텍스트는 소스코드를 실행하는데 필요한 정보를 제공한다. 여기서 소스코드가 실행되어 변경된 내용이 있다면 변경된 내용이 실행 컨텍스트에 반영된다.

앞에서 공부한 변수의 선언과 할당에서 변수는 선언, 초기화, 할당의 과정을 거친다고 공부했다. 해당 관점에서 다시 살펴보면 소스코드의 평가 단계에서 변수의 선언과 undefined로의 초기화가 일어나고 소스코드의 실행 단계에서 할당이 일어난다고 볼 수 있다.

  • 소스코드의 평가 : 실행 컨텍스트를 생성하고 변수, 함수 등의 선언문만 먼저 실행하여 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환결 레코드)에 등록한다.
  • 소스코드의 실행 : 선언문을 제외한 소스코드가 순차적으로 실행(런타임이 실행)되고, 실행 결과는 다시 실행 컨테스트가 관리하는 스코프에 등록된다.

자바스크립트 엔진의 소스 평가와 실행

다음 코드를 보고 자바스크립트 엔진이 해당 코드를 어떻게 평가하고 실행시키는지 살펴보자.

const x = 1;
const y = 2;

function foo(a) {
  const x = 10;
  cosnt y = 20;
  
  console.log(a + x + y);
}

foo(100);

console.log(x + y);
  1. 먼저 자바스크립트는 전역 코드를 평가한다. 이 과정에서 전역에 있는 모든 선언문을 실행시킨다. 위 코드에서는 x, y, foo함수가 선언 및 등록되었다. (각각이 1, 2, 함수실행이 되었다는 것이 아니라 말그대로 'x, y, foo함수가 있다'정도로만 인식됨)

  2. 전역 코드의 평가가 끝나면 소스코드를 실행한다. 위 코드에서 선언문을 제외하고 첫 소스코드는 foo(100);이므로 foo함수를 실행시킨다.

  3. 함수가 실행되면 해당 함수 코드의 평가가 일어난다. 당연히 선언문만 실행된다. 해당 함수 코드에서는 x, y가 선언되었다.

  4. 함수 코드의 평가가 끝나면 함수 코드의 실행이 일어난다. 해당 함수 코드에서 선언문을 제외한 첫 소스 코드는 console.log(a + x + y);이므로 console을 찾게 되는데 자신의 스코프에서 console을 찾을 수 없기 때문에 상위 스코프로 계속 올라가면 검색한다.
    전역 객체에서 console을 찾은 뒤, console의 프로토타입 객체에서 log 프로퍼티를 찾은 뒤 a, x, y를 찾는데 자신의 스코프에서 모두 찾을 수 있으므로 각각은 100 + 10 + 20 -> 130으로 평가되어 130을 출력한다.

  5. 함수 코드에서 모든 코드의 실행을 끝내고 다시 함수를 호출하기 전인 전역 코드로 돌아와 다음 코드인 console.log(x + y);를 실행시키게 된다. console과 log는 이전과 같은 방법으로 찾게되며 x와 y는 자신의 스코프에서 검색하여 1 + 2 -> 3으로 평가되어 3을 출력하고 해당 전역 코드의 실행을 마친다.

이처럼 코드가 실행되려면 다음과 같이 스코프, 식별자, 코드 실행 순서 등의 관리가 필요하다. 이 모든 것을 관리하는 것이 바로 실행 컨텍스트이다. 좀 더 구체적으로 말하면 실행 컨텍스트는 식별자(변수, 함수, 클래스 등의 이름)를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.

식별자와 스코프는 실행 컨텍스트의 렉시컬 환경으로 관리하고 코드 실행 순서는 실행 컨텍스트 스택으로 관리한다.

실행 컨텍스트 스택

다시 한 번의 위의 예제 코드를 보도록 하자.

const x = 1;
const y = 2;

function foo(a) {
  const x = 10;
  cosnt y = 20;
  
  console.log(a + x + y);
}

foo(100);

console.log(x + y);
  1. 위의 코드를 보면 맨 처음에 빈 실행 컨텍스트에 전역 코드가 평가되어 전역 실행 컨텍스트가 들어가게 된다. (위 설명의 1)

  2. 전역 코드가 평가되고 실행되면서 foo함수가 실행되면 foo함수의 평가가 일어나는데 이 과정에서 foo함수 실행 컨텍스트가 생성되어 스택에 담기게 된다. (위 설명의 3)

  3. foo함수내의 console.log(a + x + y);가 실행되고 foo함수의 실행이 끝나면 실행 컨텍스트 스택에서 foo함수 실행 컨텍스트가 제거된다. (위 설명의 4 이후)

  4. 그럼 다시 전역 실행 컨텍스트로 돌아와 전역 코드가 실행되는데 전역 코드이 마지막 코드인 console.log(x + y);가 실행되고 나면 전역 실행 컨텍스트가 제거된다. (위 설명의 5 이후)

위 과정을 그림으로 나타내면 다음과 같다.
실행 컨텍스트 스택

실행 컨텍스트 렉시컬 환경

렉시컬 환경은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를기록하는 자료구조로 실행 컨텍스트를 구성하는 컨포넌트이다.

본래 실행 컨텍스트는 LexicalEnvironment 컴포넌트와 VariableEnvironment 컴포넌트로 구성되는데 이 둘을 초기에 하나의 동일한 렉시컬 환경을 참조하고 있다. 이후 특수한 상황을 거치면 둘의 내용이 달라지는 경우도 있는데 해당 도서에서는 strict mode와 eval코드, try/catch 문과 같은 특수한 상황은 제외하고 LexicalEnvironment 컴포넌트와 VariableEnvironment 컴포넌트를 구분하지 않는 하나의 렉시컬 환경으로 통일해 설명한다.

렉시컬 환경은 전역과 함수에 따라 전역 렉시컬 환경과 함수 렉시컬 환경으로 나뉜다.

  • 전역 렉시컬 환경 : 전역 환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성
  • 함수 렉시컬 환경 : 함수 환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성

전역 환경 레코드 : 객체 환경 레코드와 선언적 환경 레코드 컴포넌트로 나뉘어 구성되어 있다.
  • 객체 환경 레코드 : 전역 렉시컬 환경을 구성하는 컴포넌트인 객체 환경 레코드는 BindingObject라고 부르는 객체와 연결되는데 여기서 BindingObject는 전역 객체이다. 전역 코드 평가 과정에서 var 키워드로 선언한 전역 변수와 함수 선언문으로 정의된 전역 함수는 전역 환경 레코드의 객체 환경 레코드에 연결된 BindingObject를 통해 전역 객체의 프로퍼티와 메서드가 된다.
  • 선언적 환경 레코드 : var 키워드로 선언한 전역 변수와 함수 선언문으로 정의한 전역 함수 이외의 선언인 let과 const 키워드로 선언한 전역 변수는 선언적 환경 레코드에 등록되고 관리된다.
  • this의 바인딩 : 전역 환경 레코드의 내부 슬롯인 [[GlobalThisValue]]에는 this에 바인딩되는 객체가 바인딩된다. 따라서 전역 코드에서 this를 참조하면 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 바인딩되어 있는 객체가 반환된다.

함수 환경 레코드 : 함수 환경 레코드

  • 함수 환경 레코드 : 매개변수, arguments 객체, 함수 내부에서 선언한 지역 변수와 중첩 함수를 등록하고 관리한다.
  • this의 바인딩 : 함수 환경 레코드의 내부 슬롯인 [[ThisValue]]에 함수내에서 쓰이는 this에 바인딩되는 객체가 바인딩된다.

외부 렉시컬 환경에 대한 참조 : 현재 평가 중인 소스코드를 포함하는 외부 소스코드의 렉시컬 환경, 즉 상위 스코프를 가리킨다. 이를 통해 단방향 링크드 리스트인 스코프 체인을 구현하는데 전역 렉시컬 환경에서는 스코프 체인의 종점이므로 null값이 들어가게 되고, 그 외에는 상위 렉시컬 환경의 참조가 할당된다.

다소 어려운 말이라 처음 읽을 때는 무슨 말인지 헷갈렸는데 포스팅을 하며 막히는 부분을 하나씩 이해하며 보니까 많이 이해가 됐다. 아래 코드와 해당 코드에 대해 렉시컬 환경을 도식화한 그림을 보고 이해해보자.

var x = 1;
constr y = 2;

function foo(a) {
  var x = 3;
  const y = 4;
}

foo(20);

렉시컬 환경

위 그림은 foo함수의 함수 코드가 평가되었을 때의 상황이며 아직 실행되기 이전이다. 따라서 함수 환경 레코드에 a와 x가 undefined로 초기화되어 있으며, cosnt로 선언된 y는 uninitialized된 상태이다.

또한 foo함수의 내부 슬롯인 [[ThisValue]]가 전역 객체를 가리키는 이유는 foo함수가 일반 함수로서 호출되었기 때문이다. 일반 함수로 호출된 함수의 내부에서 this에 바인딩되는 값은 항상 전역 객체이다.

블록 레벨 스코프

ES6 이후에 등장한 let과 const 키워드로 선언한 변수는 블록 레벨 스코프를 가진다. 실행 컨텍스트를 실행하다가 블록을 만나게 되면 블록 렉시컬 환경을 만들어 기존의 렉시컬 환경을 블록 렉시컬 환경으로 교체하고, 블록 렉시컬 환경의 환경 레코드의 선언적 환경 레코드에서 let이나 const 키워드로 선언한 변수를 관리하게 된다. 또한 해당 블록 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 블록문이 실행되기 이전의 렉시컬 환경을 가리키게 된다.

profile
I Will be Relaxed Person

0개의 댓글