자바스크립트 딥다이브 - 실행 컨텍스트

ChoiYongHyeun·2023년 12월 15일
0

실행 컨텍스트를 통해 자바스크립트가 어떻게 스코프를 기반으로 코드를 실행하는지 알아보자

정말 책에서 너~무나도 친절하고 완벽하게 설명해준다.

소스코드의 타입

소스 코드 유형설명예시
전역 소스 코드전역 범위에서 실행되는 코드. 전역 컨텍스트에서 관리되며, 스크립트가 시작될 때 실행됨.전역 변수, 전역 함수 등
함수 코드함수 내에서 정의된 코드. 함수가 호출될 때 함수 컨텍스트에서 실행됨.지역 변수, 매개변수, 내부 함수 등
eval 코드문자열로 표현된 코드를 실행하는 데 사용. 전역 컨텍스트나 호출한 컨텍스트에서 실행될 수 있음.동적으로 생성된 코드, 사용은 권장되지 않음
모듈 코드ES6에서 도입된 모듈을 정의하는 코드. 모듈은 독립적인 스코프를 갖으며, import/export 문을 사용.모듈 스코프, 외부에서 노출하고 가져오는 변수 및 함수

소스 코드 유형에는 크게 4가지 유형이 있는데

4가지 소스코드 마다 실행컨텍스트를 다르게 생성하여 실행하기 때문이다.

실행컨텍스트에 대한 내용은 추후 다루도록 하겠다.

소스코드의 평가와 실행

모든 소스코드는 런타임 전 평가 과정을 거친다.

소스코드를 평가하고 , 컨텍스트에 맞춰 실행한다.

소스코드를 평가하는 단계에서는 소스코드 별로 실행 컨텍스트 를 생성하고 호이스팅 되는 내용들을 실행 컨텍스트 내부 스코프 단위에 등록한다.

소스코드 평가 과정이 끝나면 소스코드에 따라 컨텍스트를 이동해가며 실행 결과를 반환한다.

실행 컨텍스트

실행 컨텍스트는 소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다.

좀 더 구체적으로 말해 소스코드에 따라 실행 컨텍스트가 생성되는데 , 실행 컨텍스트 내에서 적절한 스코프를 따라 식별자 및 함수들을 관리한다.

var x = 1;

function foo() {
 var x = 10;
}

예를 들어 해당 소스코드를 자바스크립트 엔진이 실행 할 때

전역 코드인 var x = 1 에 대해서 전역 컨텍스트에 x : 1 로 전역 객체를 관리하고

함수 코드인 function foo() {...} 에 대해서 함수 컨텍스트를 생성하고 , 함수 컨텍스트 내에 x : 10 을 선언한다.

평가 할 때에는 컨텍스트 내에 식별자들과 함수들에 대해서 undefined 로 해둔다.
실행 될 때 비로소 문맥에 맞춰 값들이 할당된다.

var 은 함수 레벨 스코프를 가졌기 때문에 함수 블록 내에서는 함수 컨텍스트 내에서 존재한다.

실행 컨텍스트 스택

자바스크립트는 평가 단계에서 소스 코드 별로 실행 컨텍스트를 생성한다고 하였다.

그럼 많은 실행 컨텍스트들을 어떻게 실행할까 ?

실행 컨텍스트 스택은 소스코드 흐름에 따라 실행 해야 할 실행 컨텍스트들을 담는 자료구조 이다.

function foo() {
  const x = 10;

  function bar() {
    const x = 100;
    console.log(x);
  }

  bar();
  console.log(x);
}

foo();

다음과 같은 코드 구조가 있다고 해보자

자바스크립트 엔진이 해당 코드를 평가 할 때 우선적으로는 전역 실행 컨텍스트를 생성한다.

이후 함수 foo 가 선언됨에 따라 함수 foo 의 함수 컨텍스트가 생성된다.

foo 함수 컨텍스트 내부에서 x 에 대해 호이스팅 결과로 x : ReferenceError 가 된다.

let , const 선언문은 호이스팅 시 선언만 되고 초기화는 일어나지 않는다.
초기화 및 할당은 호출과 함께 실행된다.
이로 인해 일시적 사각지대에 위치하게 된다.

foo 블록문을 실행 하던 중 bar 이 선언됨에 따라 bar 의 함수 컨텍스트가 생성되고 bar 함수 컨텍스트 내부에도 x : ReferenceError 가 된다.

정리

현재 실행 컨텍스트들은 [전역 실행 컨텍스트 , foo 컨텍스트 , bar 컨텍스트] 가 존재하며
각 컨텍스트 별로 식별자들이 컨텍스트에 맞는 스코프에 따라 연결되어 있다.

자 선언은 끝났고 실행해보자

function foo() {
  const x = 10;

  function bar() {
    const x = 100;
    console.log(x);
  }

  bar();
  console.log(x);
}

foo();

소스코드가 실행 될 때 우선 전역 실행 컨텍스트를 실행한다.

실행 컨텍스트 스택 : [전역 실행 컨텍스트]

이후 함수 foo() 가 실행 되었기 때문에 foo 실행 컨텍스트 에 들어가 함수 내부의 소스코드를 실행한다.

실행 컨텍스트 스택에 foo 실행 컨텍스트가 담긴다.

실행 컨텍스트 스택 : [전역 실행 컨텍스트, foo 실행 컨텍스트]

foo 실행 컨텍스트 내부에서 지역 변수 const x = 10 가 실행되었기 때문에 foo 실행 컨텍스트 안의 프로퍼티로 x : 10 을 할당한다.

foo 실행 컨텍스트 에서 실행 중 bar() 가 실행되었기 때문에 bar() 의 함수 몸체에 들어가 소스코드를 실행한다.

실행 컨텍스트 스택 : [전역 실행 컨텍스트, foo 실행 컨텍스트 , bar 실행 컨텍스트]

bar 실행 컨텍스트 내부에서 const x = 100이 실행 되었기 때문에 bar 실행 컨텍스트 안의 프로퍼티로 x : 100 을 할당한다.

이후 console.log(x) 를 실행하고 bar 실행 컨텍스트 에서 나온다.

모든 컨텍스트를 시행했기 때문에 종료된다.

이로 인해 실행 컨텍스트 스택 에서 bar 실행 컨텍스트 는 빠져 나온다.

실행 컨텍스트 스택 : [전역 실행 컨텍스트, foo 실행 컨텍스트]

foo 실행 컨텍스트에서 이후 소스인 console.log(x) 를 실행하고 모든 소스코드를 실행했기 때문에 foo 실행 컨텍스트 에서 빠져 나오며 실행 컨텍스트 스택 에서도 제거된다.

실행 컨텍스트 스택 : [전역 실행 컨텍스트]

모든 실행이 끝났기 때문에 전역 실행 컨텍스트 도 종료되고 스택에서 제고된다.

실행 컨텍스트 스택 : []

위 과정을 통해 알 수 있던 것은 소스코드 순서에 따라 실행 컨텍스트 스택 에 실행 해야 할 실행 컨텍스트들이 담기고 가장 위에는 현재 실행중인 실행 컨텍스트가 담긴다는 것이다.

렉시컬 환경

실행 컨텍스트는 LexicalEnvironmentVariableEnvironment 컴포넌트로 이뤄져있는데 두 컴포넌트는 렉시컬 환경을 참조하고 있다.

책에서는 두 컴포넌트가 참조하고 있는 렉시컬 환경을 기준으로 설명을 이어갔기에 나 또한 그렇게 정리하겠다.

렉시컬 환경은 실행 컨텍스트 내부에서 식별자와 식별자의 값, 상위 스코프에 대한 참조를 기록하는 자료구조이다.

실행 컨텍스트는 소스 코드에 따라 생성되어 컨텍스트 내부 스코프를 가지게 되는 문맥이라면

안에서 식별자와 스코프 체인은 렉시컬 환경에서 관리된다.

렉시컬 환경은 실행 컨텍스트의 실체와 같은 객체라고 생각하자

렉시컬 환경은 두 가지 컴포넌트로 구성되어 있다.

  • EnvironmentRecord : 환경 레코드로 스코프에 포함된 식별자를 등록하고 식별자에 바인딩 된 값을 관리하는 저장소
  • OuterLexicalEnvironmentRefrence : 상위 렉시컬 환경을 가리키는 스코프로 실행 컨텍스트 별로도 체인이 있듯, 렉시컬 환경또한 단방향 연결리스트 형태로 스코프 체인으로 이뤄져있다.
var x = 10;

function foo() {
  var y = 100;
  console.log(x + y);
}

foo(); // 110

다음 함수를 실행하면

전역 실행 컨텍스트에서 전역 렉시컬 환경 내에 BindingObject 라는 객체의 프로퍼티로 x : 10 이란 값과 foo 라는 함수를 추가한다.

foo 라는 함수는 함수 실행 컨텍스트 에서 함수 렉시컬 환경내에 BindingObject 객체의 프로퍼티로 y : 10이라는 값을 프로퍼티로 추가한다.

또한 foo 의 함수 실행 컨텍스트 의 상위 렉시컬 환경으로 전역 렉시컬 환경을 OuterLexicalEnvironementRefrence 에서 가리키고 있다.

코드를 실행하면 foo 의 함수 실행 컨텍스트 에서 전역 렉시컬 환경 에 존재하는 프로퍼티 x 에 접근 할 수 있는 이유도 foo 의 함수 실행 컨텍스트 내부에 프로퍼티 x 가 존재하지 않기 때문에 상위 실행 컨텍스트에 접근하여 프로퍼티 x 에 접근하기 때문이다.

전역 실행 컨텍스트 살펴보기

위에서 실행 컨텍스들에 대한 개괄적인 내용을 정리해보았다.

그러면 이제 각 실행 컨텍스트들을 살펴보고 나중에 코드와 함께 살펴보자

전역 실행 컨텍스트 안엔 LexicalEnviornment 라는 객체가 존재하며 해당 객체는 전역 렉시컬 환경 을 가리키고 있다.

그럼 전역 렉시컬 환경 을 살펴보자

전역 렉시컬 환경 안에도 GlobalEnviornment 라는 객체가 존재하며 해당 객체는

  • 전역 렉시컬 환경 내에 존재하는 객체를 관리 하고 있는 BindingObject 객체
  • 전역 렉시컬 환경 내에서 선언적 렉시컬 환경(Declarative Environment Record)

두 객체를 가리키고 있다.

선언적 렉시컬 환경

이전 전역 스코프에서 선언된 let , constvar 과 다르게 개별적인 렉시컬 환경에서 저장된다고 하였다.
전역 스코프에서 선언된 let , const 들로 생성된 객체는 모두 선언적 렉시컬 환경에서 저장된다.

BindingObject 은 전역 객체인 window 와 연결되어 있다.

그렇기에 전역 실행 컨텍스트 내에서 정의된 프로퍼티와 메소드들은 모두 window 의 프로퍼티로 바인딩 된다.

전역 실행 컨텍스트에서 프로퍼티와 메소드만 관리하는 것이 아닌

this 가 누군가를 가리키는지 바인딩 해놔야 한다.

전역 실행 컨텍스트에서this전역 객체(global or window) 와 바인딩 된다.

또 모든 실행 컨텍스트들은 상위 실행 컨텍스트를 가리키는 OuterLexicalEnviromentRefrence 가 존재한다.

전역 객체의 경우엔 상위 실행 컨텍스트가 없기 때문에 null 이다.

함수 실행 컨텍스트 생성

함수 실행 컨텍스트도 전역 실행 컨텍스트와 큰 차이는 없다.

함수 실행 컨텍스트도 함수 렉시컬 환경을 통해 관리된다.

전역 렉시컬 환경에선 BindingObject 객체를 통해 식별자와 메소드가 관리되었다면

함수 렉시컬 환경에선 FunctionEnvironemtRecord 객체를 통해 관리된다.

해당 객체에는 매개변수 , arguments , 함수 컨텍스트 내에서 선언된 식별자 및 함수 등이 관리된다.

OuterLexicalEnviromentRefrence 는 함수로 실행 되었을 경우에는 전역 객체를 가리킨다.

this 는 상황에 따라 다르게 바인딩 된다.

정말 그림으로 잘 설명된 것이 있는데 올릴 수가 없다

책의 이미지를 함부러 올렸다가 저작권 문제가 생길까 두려워 올리지 못하나 만약 다른 블로그에 정리된 것이 있다면 찾아보기를 바란다
아니면 책을 구매하거나 , 책 정말 최고 키킥

실행 컨텍스트와 블록 레벨 스코프

for (let i = 0; i < 10; i += 1) {
  console.log(i);
} // for 문 안에 존재하는 i 는 어떤 렉시컬 환경에서 관리 될까 ? 

let , const 는 블록 레벨 스코프라고 했다.

블록 레벨 스코프가 존재한단 것은 블록 렉시컬 환경도 존재한다는 것이다.

함수와 같은 경우는 선언을 통해 실행 컨텍스트를 생성했지만 블록문이나 조건문 과 같은 다양한 문들은 어떻게 관리 될까 ?

그것은 전역 실행 컨텍스트에서 해당 문이 실행 되는 동안 렉시컬 환경을 잠깐 블록 렉시컬 환경으로 변경하면서 해결한다.

블록 렉시컬 환경에서 let , const 로 선언된 변수들을 관리하기 위해 블록 렉시컬 환경내에 선언적 렉시컬 환경 안에 변수들을 관리한다.

또한 OuterLexicalEnvironmentRefrence 에서는 기존 전역 실행 컨텍스트에서 가리키고 있던 전역 렉시컬 환경을 가리킨다.

//전역 렉시컬 환경을 가리키고 있음
const i = 5;

//블록 렉시컬 환경을 가리키고 있음
for (let i = 0; i < 5; i += 1) {
  console.log(i);
}
// 종료와 함께 다시 전역 렉시컬 환경을 가리키고 블록 렉시컬 환경은 제거됨 (가비지 컬렉터)

위 코드에서 동일한 식별자 명인 i 를 사용해도 for 문이 문제 없이 작동하는 이유는

두 식별자가 다른 렉시컬 환경에서 선언되고 관리 되고 있기 때문이다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글