Frontend/JS

자바스크립트 - Execution Context (실행 컨텍스트)

Creative_Lee 2022. 3. 23. 03:14

1. Execution Context (실행 컨텍스트) 란?

scope, hoisting, this, function, closure 등의 동작 원리를 담고있는 자바스크립트의 핵심 원리입니다.

실행 가능한 코드가 실행되기 위해 필요한 환경이라고 말할 수 있습니다.

자바스크립트 엔진에 의해 실행 컨텍스트 내부에는 코드 실행에 필요한 정보들이 저장됩니다.

 


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

아래 코드는 실행 컨텍스트의 생성과 삭제 과정을 잘 표현해 주는 코드입니다.

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  bar();
}
foo();

코드의 실행 과정을 실행 컨텍스트의 관점으로만 살펴보면 생성, 삭제 과정은 다음과 같습니다.

 

1. 코드를 실행하기 전, 실행 컨텍스트를 쌓아나갈 실행 컨텍스트 스택(Stack)이 먼저 생성됩니다.

  

2. 처음 전역 코드를 실행하려 하면 전역 실행 컨텍스트(global EC)가 생성되고 이것이 실행 컨텍스트 스택에 쌓입니다.

  

3. 현재 실행 중인 컨텍스트와 관련없는 코드가 실행되면 (ex 새로운 함수),
   새로운 컨텍스트를 생성하고 이전 컨텍스트 위에 쌓이며 컨트롤(제어권)도 이동합니다.

 

4. 실행이 끝나면 해당 실행 컨텍스트를 삭제하고 직전의 실행 컨텍스트로 컨트롤이 이동합니다.

 

5. 전역 실행 컨텍스트는 웹 페이지를 종료하기 전까지 사라지지 않습니다.

 

 

위의 생성, 삭제 과정을 이해하고 3번으로 넘어가 봅시다.


3. 실행 컨텍스트의 3가지 객체 프로퍼티

자바스크립트의 엔진은 코드를 실행하기 위해 다음과 같은 정보를 알고 있어야 합니다.

  • 변수
  • 변수의 Scope
  • 함수 선언
  • this

 

때문에 엔진은

실행 컨텍스트를 물리적으로 객체의 형태로 관리하고 3개의 프로퍼티로 위 정보들을 보관합니다.

실행 컨텍스트의 3가지 프로퍼티는 다음과 같습니다.

3가지 객체 프로퍼티

 

3-1. 변수 객체 (Variable Object)

[변수, 매개변수와 인자, 함수 선언]에 대한 정보를 담는 객체입니다.

코드가 실행될 때 엔진에 의해 참조되며 코드에서는 접근할 수 없습니다.

 

Variable Object는 실행 컨텍스트의 프로퍼티이므로 값을 가지는데,
이 값은 또 다른 객체를 가리킵니다.

 

전역 컨텍스트와 함수 컨텍스트의 Variable Object 서로 다른 객체를 가리킵니다.

전역 코드와 함수 코드의 내용이 다르기 때문입니다.

간단한 예로 전역 코드에는 매개변수가 없지만 함수 코드에는 매개변수가 있는 것 처럼 말이에요!

 

각각의 경우에 Variable Object가 가리키는 객체는 다음과 같습니다.

전역 컨텍스트(Global Execution Context)의 경우

  • Variable Object는 전역 객체(Global Object)를 가리킵니다.
  • 전역 객체는 최상위에 위치하면서 유일하고, 모든 전역 변수, 전역 함수 등을 포함합니다.

위의 코드의 경우 전역 컨텍스트의 변수 객체는 전역 객체를 가리킨다.

 

함수 컨텍스트(Function Execution Context)의 경우

 

  • Variable Object는 활성 객체(Activation Object)를 가리킵니다.
  • 활성 객체에는 매개변수와 인자들의 정보를 배열의 형태로 담고있는 객체인
    arguments object가 추가되어 있습니다.

 

위의 코드의 경우 함수 컨텍스트의 변수 객체는 활성 객체를 가리킨다.


3-2. 스코프 체인(Scope Chain)

Scope Chain은 식별자 중에서 변수를 검색하는 메커니즘으로 사용되며(전역 객체를 제외한 객체가 아닌 식별자)

해당 전역 or 함수 컨텍스트의 Variable Object가 가리키는

전역 객체(GO), 활성 객체(AO)의 참조로 이루어진 리스트입니다.

 

리스트는

현재 실행 컨텍스트의 활성 객체 -> 상위 컨텍스트의 활성 객체 -> 전역 객체 의 순서로 이루어집니다.

 

*Scope(스코프)란?

- 현재 접근할 수 있는 변수들의 범위입니다.

- 즉 Variable Object인 GO, AO라고 할 수 있습니다!

 

foo함수 컨텍스트의 스코프 체인은 0 : AO -> 1: GO 인 리스트를 가리킨다.

함수 실행중에 변수를 만나면
Scope Chain을 통해 순서대로 AO - AO - .... GO 까지 검색을 이어 나갑니다.

최종까지 실패하면 정의하지 않은 변수에 접근 하는것으로 판단하고 에러를 발생시킵니다!

자바스크립트 엔진은 Scope Chain을 통해 렉시컬 스코프를 파악합니다. 

 

*렉시컬 스코프(Lexical scope)란?

  - 변수 혹은 함수가 생성된(선언된) 장소(위치)입니다.

 

  - 변수 혹은 함수가 호출된 위치가 반드시 렉시컬 스코프 인건 아니지만
    변수 혹은 함수가 정의된 위치는 반드시 렉시컬 스코프 입니다.

  - 자바스크립트에서는 어떤 변수 혹은 함수가 선언된 위치를 기준으로 상위 스코프를 결정합니다.
     즉 렉시컬 스코프를 기준으로 상위 스코프를 결정합니다.

 

 

함수가 중첩 될 때마다 부모 함수의 Scope가 자식 함수의 Scope Chain에 포함되므로

하위 함수에서 상위 함수의 스코프와 전역 스코프까지 참조가 가능합니다.

 

Scope Chain은 함수의 숨김 프로퍼티인 [ [ Scope ] ] 로 참조할 수 있습니다.

 

foo의 스코프 체인

 


3-3. this

this 프로퍼티에는 this값이 할당됩니다.
자바스크립트에서는 함수 호출 방식에 의해 this에 바인딩할 객체가 동적으로 결정됩니다.

 


4. 실행 컨텍스트의 생성, 삭제의 세부 과정!

사실 실행 컨텍스트 생성, 삭제 과정은 2번처럼 간단하지 않습니다!

세부 과정을 살펴봅시다.


1. 전역 코드 진입 이전

전역 코드 진입 이전 상태

제일 처음 실행 컨텍스트 스택유일한 전역 객체(Global Object)가 생성됩니다.

전역 객체의 프로퍼티는 코드의 어떠한 곳에서도 접근 가능합니다.
초기 상태의 전역 객체에는 빌트인 객체와 BOM, DOM이 설정되어 있습니다!

 


2. 전역 코드 진입

전역 코드 진입 상태

전역 실행 컨텍스트(Global EC)가 생성되고 스택에 쌓입니다.

이후 전역 실행 컨텍스트를 바탕으로 
Scope chain의 생성과 초기화, 변수 객체화, this value 결정 등의 작업이 일어납니다.

 

 

2-1. Scope chain의 생성과 초기화

Scope chain의 생성

전역 실행 컨텍스트의 Scope chain이 생성, 초기화 됩니다.


Scope chain은 전역 객체(Global Object)의 레퍼런스를 포함하는 리스트가 됩니다.


 

2-2. Variable Instantiation (변수 객체화) 실행

Variable Object에 프로퍼티와 값 추가

변수 객체화가 실행됩니다.


변수 객체화는 Variable Object에 프로퍼티와 값을 추가하는 것을 의미합니다.
Global Excuetion Context의 경우 Variable Object은 전역 객체(GO)를 가리킵니다.

변수 객체화는 반드시 아래의 순서로 VO에 프로퍼티와 값을 설정합니다.
   1. (함수 코드인 경우) 매개변수 -> 프로퍼티, 인수 -> 값으로 설정됩니다.
   2. 코드 내의 함수 선언(함수 표현식 x)을 대상으로 함수명 -> 프로퍼티, 함수 객체 -> 값으로 설정됩니다.      
   3. 코드 내의 변수 선언을 대상으로 변수명 -> 프로퍼티, undefined -> 값으로 설정됩니다.

 


생성된 foo 함수 객체의 내부 프로퍼티 [[Scopes]]

예제 코드에서는 위 순서에 따라 함수 선언인 'foo'가 먼저 객체화 됩니다.
GO
 프로퍼티로 foo , 값으로 foo의 함수 객체가 설정됩니다.

이때 생성된 함수 객체는 [[Scopes]] 라는 프로퍼티를 가지게 됩니다.
[[Scopes]] 프로퍼티는 '함수 객체가 실행되는 환경' 을 가리킵니다.
즉 현재 Scope chain이 참조하는 객체를 값으로 설정합니다.

 

이로써 함수 객체의 [[Scopes]] 프로퍼티는
자신의 실행 환경(AO) + 자신보다 상위 함수의 실행 환경(AO) + 최상위의 전역 환경(GO) 을 모두 가리키게 되는데,

 

이때 자신보다 상위 함수의 실행 컨텍스트가 스택에서 소멸하여도

[[Scopes]] 프로퍼티가 가리키는 상위 함수의 실행 환경들은 그대로 남아있어 참조가 가능합니다.

이것이 클로저(Closure)의 원리입니다!

 

또한  함수 선언 코드를 실행하기 전에 함수를 호출하여도 VO에 이미 함수가 등록되어 있기 때문에

함수 선언 코드 이전에서도 함수 사용이 가능합니다.

(단 함수 표현식을 사용한 경우 해당 변수 키워드의 객체화 방식을 따릅니다.)

이것이 함수 호이스팅(Function hoisting)의 원리입니다.

 


객체화 되는 'x'

이어서 변수 선언인 'x' 가 객체화 됩니다.

GO 프로퍼티로 x , 값으로 'undefined' 가 설정됩니다.

과정을 세분화 하면 다음과 같습니다.

 

1. 선언 단계

  • VO에 변수를 등록합니다.
  • VO는 스코프가 참조 가능한 대상이 됩니다.

2. 초기화 단계

  • VO에 등록된 변수를 메모리에 할당합니다. 
  • 변수를 undefined로 초기화 합니다.

3. 할당 단계

  • undefined로 초기화된 변수에 실제값을 할당합니다.

 

var 키워드로 선언된 변수는 선언, 초기화 단계가 한번에 이루어집니다.

즉 변수를 VO에 등록함과 동시에 undefined로 초기화됩니다.

 

이로써 변수 선언 코드를 실행하기 전에 변수에 접근하여도 VO에 이미 변수가 등록되어 있기 때문에

에러가 발생하지 않습니다. (undefined를 리턴합니다)

이것이 변수 호이스팅(Variable Hoisting)의 원리입니다!!

 

 

2-3. this value 결정

this의 value

변수 선언 과정이 끝나면 this의 value가 결정됩니다.

this value가 결정되기 이전에 this는 전역 격체(Global Object)를 가리키고 있다가
함수 호출 패턴에 의해 this에 할당되는 값이 결정됩니다.

 

전역 컨텍스트(전역 코드)의 경우에는
Variable Object, Scope chain, this의 값은 언제나 전역 격체(Global Object) 입니다.

 


3. 본격적인 코드의 실행

지금까지는 코드의 실행 환경을 갖추기 위한 과정이었습니다!

이제 드디어 코드를 실행해 봅시다.

 

 

3-1. x변수에 값 할당

전역 변수 x 에 문자열을 할당합니다.

변수명에 해당하는 프로퍼티를 찾았습니다.

전역 변수 x에 문자열 'xxx'를 할당할 때,

현재 실행 컨텍스트의 Scope chain이 참조하는 VO를 맨 앞부터 검색합니다.

변수명에 해당하는 프로퍼티를 발견하면 값을 할당합니다.


3-2. foo함수의 선언 실행

foo함수 선언 코드를 실행합니다.

새로운 함수 실행 컨텍스트 생성

새로운 함수 실행 컨텍스트가 생성됩니다.

Scope chain의 생성과 초기화 -> Variable Instantiation(변수 객체화) -> this value 결정이 순차적으로 실행됩니다.

함수 실행 컨텍스트의 위 3가지 과정은 전역 실행 컨텍스트의 과정과는 차이가 있습니다.

 

 

3-2-1. Scope chain의 생성과 초기화

AO가 생성되고 SC에서 참조한다.

함수 코드는 Activation Object(AO)에 대한 레퍼런스를 Scope chain의 맨앞에 설정하는 것으로 시작됩니다.

이후 AO는 arguments 프로퍼티의 초기화를 실행합니다.
(매개변수와 인수들의 정보를 배열의 형태로 담고있는 객체)

 

Caller의 [[Scope]]를 함수 컨텍스트의 Scope chain에 push합니다.

다음으로

Caller의 Scope chain이 참조하는 객체를 함수 컨텍스트의 Scope chain에 push합니다.

(Caller의 [[Scope]]를 함수 컨텍스트의 Scope chain에 push합니다. 라고도 해석 가능합니다.)

 

코드에서는 global EC의 Scope chain이 참조하는 GO가 foo() EC의 Scope chain에 push됩니다. 

 

*AO는 스펙상의 개념으로 프로그램에서 접근 불가능합니다. (AO의 프로퍼티는 접근 가능합니다.)

 

 

 

3-2-2. Variable Instantiation(변수 객체화) 실행

변수 객체화로 선언 처리된 bar함수와 함수 객체의 프로퍼티인 [[Scopes]]

함수 코드의 경우,
Scope chain의 생성과 초기화 단계에서 생성된 AO를 VO로서 변수 객체화가 실행됩니다.

나머지는 전역 코드의 경우와 같습니다.

 

먼저 bar 함수의 선언 처리가 먼저 진행 되고 AO에 바인딩되면, bar 함수 객체의 프로퍼티로 [[Scopes]]가 생성됩니다.

이때 [[Scopes]] 프로퍼티의 값은 AO와 GO를 참조하는 리스트, 즉 SC가 됩니다.

 

변수 객체화로 선언 처리된 변수 y

이후 변수 y를 선언 처리 합니다.

변수 y를 AO에 등록하고 메모리에 할당한 뒤, undefined로 초기화 합니다.

 

 

 

3-2-3. this value 결정

this의 value는 함수 호출 패턴에 의해 결정됩니다.

내부 함수의 경우, this의 value는 GO 입니다.

 

 


 

3-3. foo 함수 내부 코드 실행

foo함수의 내부에는 변수 y에 대한 문자열 할당, bar 함수 선언 코드, bar 함수 실행 코드가 있습니다.

 

3-3-1. 변수 y에 값 할당

sc을 통해 AO에 접근하여 y를 찾았습니다.

문자열 할당 코드가 실행되고,

SC이 참조하는 AO에서 변수명 y에 해당하는 프로퍼티를 찾았습니다.

(만약 찾지 못했다면, SC이 참조하는 다음 객체, GO에서 검색을 이어갔을 겁니다.)

 

변수 y에 문자열 'yyy'가 할당 되었습니다.

 

 

3-3-2. bar 함수의 선언 실행

bar() EC의 생성과 3가지 동작

bar함수의 선언 실행과 함께 새로운 EC가 생성되고

Scope chain의 생성과 초기화 -> Variable Instantiation(변수 객체화) -> this value 결정이 순차적으로 실행됩니다.

 

SC의 생성과 초기화 실행

  • SC는 AO-2의 래퍼런스를 맨 앞에 설정하고 arguments 프로퍼티를 초기화 합니다. 
  • foo() EC의 SC가 참조하는 AO-1과 GO, 즉 [[Scopes]]를 SC에 push 합니다. 
  • SC는 AO-2, AO-1, GO 의 순서로 자신과 자신의 상위 함수, 전역 스코프까지 참조 가능합니다.

변수 객체화의 실행

  • 변수 z를 AO-2에 등록하고 메모리에 할당한 뒤 undefined로 초기화 합니다.

this value 결정

  • 함수의 호출 패턴에 의해 결정됩니다.
  • 내부 함수이므로 this의 value는 GO입니다.

 

 

3-3-3. bar 함수의 실행

z변수의 값 할당

bar 함수가 실행되고,

변수 z에 'zzz' 를 할당하는 코드가 실행됩니다.

 

SC이 참조하는 AO-2에서 변수명 z에 해당하는 프로퍼티를 찾았고 변수 z에 문자열 'zzz'가 할당 되었습니다.

(만약 찾지 못했다면, SC이 참조하는 다음 객체, AO-1 에서 검색을 이어갔을 겁니다. 또 찾지 못하면 ... GO까지...)

 

 

이어서 console.log(x+y+z) 가 실행됩니다.

x는 AO-2에 없습니다. -> AO-1에 없습니다. -> GO에서 x를 찾았습니다. 값은 'xxx' 입니다.

y는 AO-2에 없습니다. -> AO-1에서 y를 찾았습니다. 값은 'yyy' 입니다.

z는 AO-2에 있습니다. -> 값은 'zzz'입니다.

 

 

4. 드디어 결과입니다.

험난한 과정을 거쳐 예제 코드는 콘솔창에 xxxyyyzzz를 출력합니다!!!!!!!!!!!!!

 

 

5. 역대 포스팅중에 제일 힘들었던 포스팅을 마치며....

Execution Context, Scope chain, closure, Lexical scope, hoisting, this 등 정말 여러가지 개념을 배울수 있었습니다.

모두 이해하기 힘들었던 만큼, 포스팅을 마무리 하는 지금 이 순간에는
모두 완벽하게 이해했고 잊어버릴 수 없는 정도입니다.


저를 유독 괴롭혔던 Lexical scope는 진짜... 절대 못잊습니다....

 

다른 국내 블로그를 찾아보는데..

'자바스크립트는 렉시컬 스코프를 따른다.....' 라며 일종의 방식처럼 설명하는 글도 있었고,

 

'함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정되는 것을 말한다.'

라며 아예 무엇을 결정하는 것인지 적지 않은 글도 있었는데

 

이것들이 저를 너무 혼란스럽게 했습니다.

그래서 렉시컬 스코프가 스코프인건지 어떠한 방식인건지 모르겠더라구요...

 

그래서 영문으로 검색해서 영어공부좀 했습니다... 

번역기가 없었다면... 진짜 죽었을지도 몰라요...

 

예.. 그냥 하소연입니다... 

 

아 그리고 모든 시각화 자료의 출처는 https://poiemaweb.com/ 입니다!

정말 좋은 레퍼런스 가득한 사이트니깐 들어가보세요.

 

끝...

오늘은 진짜 일찍 자려고 했는데 결국엔 3시에 마무리하며....