FeeL
운 좋게 포인트 30만 원에 선정되어서 그 돈으로 읽고 싶던 책을 구매할 예정이다. 내가 현재 자주 접하는 자바스크립트와, 자바의 동작 방식 및 코어 개념을 좀 더 알고 싶어서 모던 자바스크립트, 코어 자바스크립트의 책을 읽기 전 사전 공부하는 목적으로 한번 전체적인 그림을 보려고 한다.
실행 콘텍스트(Execution Context)는 scope, hoisting, this, function 등의 동작 개념을 포괄하고 있는 자바스크립트의 핵심원리로 써 학습하기에 구성이 상당히 복잡한 편에 속한다. 실행 콘텍스트는 자바스크립트 스펙을 위한 메커니즘이라 그렇기도 하다. 하나씩 접근하면서 각 부분을 알아보자.
Execution Context | ||||||
code evaluation state | Function | Realm | ScriptOrModule | Generator | Lexical Environment |
Variable Environment |
실행 컨텍스트는 실행 가능한 코드가 실행되기 위해 필요한 환경을 제공하는 객체이다.
실행 가능한 코드란 전역 영역에 존재하는 코드, eval() 함수로 실행되는 코드, 함수 영역에 존재하는 코드, 모듈 코드로 나누어질 수 있다.
일반적으로 우리는 전역 코드와 함수 내부 코드, 그리고 모듈 코드가 일반적으로 사용된다.
이러한 스펙을 구현한 자바스크립트 엔진은 코드를 실행하기 위해 여러 가지 정보를 알고 있어야 한다.
변수, 함수 선언, scope, this 등 이와 같이 실행에 필요한 정보를 형상화하고 구분하기 위해 자바스크립트 엔진은 실행 콘텍스트를 물리적 객체의 행태로 관리한다.
모든 코드의 실행에는 Global EC(실행 콘텍스트 스택)를 가지고 시작한다. 위 그림과 같이 자바스크립트의 실행 가능한 코드가 실행되면 실행 콘텍스트가 스택에 생성되고 소멸된다. 현재 실행 중인 콘텍스트에서 관련 없는 코드가 실행되면 새로운 콘텍스트가 생성된다. 이렇게 생성된 콘텍스트에게 제어권이 이동한다.
흐름
- 실행가능 한 코드에서 논리적인 실행 콘텍스트 스택이 생성되고 전역 코드로 제어권이 들어오면 전역 실행 컨택스트가 생성되고 실행 콘텍스트 스택에 쌓인다. 이 전역 실행 콘텍스트는 애플리케이션이 종료될 때까지 유지된다.
- 함수를 호출하면 해당 함수의 실행 콘텍스트가 생성된다. 직전에 실행된 코드 블록의 실행 콘텍스트 위에 쌓이며, 함수의 실행이 종료되면 해당 함수의 실행 콘텍스트를 종료하고, 직전의 실행 콘텍스트에 제어권을 양도한다.
실행 컨텍스트는 어찌 보면 실행 가능한 코드를 영역 별로 구분하여 스택에 쌓는 개념이지만 물리적으로 객체의 형태를 가지고 다양한 속성을 통해 실행 영역의 코드를 제어할 수 있다.
따라서 상위의 실행 컨택스트의 구성도에 있는 수많은 속성들을 활용하여 소스코드 실행에 필요환 환경을 제공하고 실행결과를 관리한다.
실행 가능 코드 평가 및 실행
- code evaluation state : 실행 콘텍스트의 코드의 실행, 중지, 재개하는 상태
- function : 함수 코드 평가 시 해당 함수의 객체 값을 포함하고 있다, script 혹은 module코드 평가 시 null 값을 가진다.
- Realm : object sets, ECMAScript 전역 환경, 해당 전역 환경의 범위 내에서 로드되는 모든 코드, 기타 관련 상태 및 리소스로 구성됩니다. 즉 현재 진행 중인 작업에 대한 글로벌 스코프에 대한 액세스를 제공하는 일종의 추상 엔티티
- ScriptOrModule : 모듈 레코드, 스크립트 레코드이며 없는 경우 null
소스코드 평가하는 과정에서는 식별자, 함수의 선언문, 변수에 해당되는 모든 코드가 평가된다.
이러한 평가 과정에서 실행 콘텍스트를 생성하고 식별자, 변수, 함수의 선언문 등은 Lexical environment의 환경 레코드에 등록되어 관리된다. 이후 실행이 끝나면 해당 코드의 실행 결과는 환경 레코드에 다시 등록된다.
실행 단계에서 렉시컬 환경에서는 식별자, 변수, 함수, 스코프 등의 정보를 관리하고, 코드의 실행 순서는 콜 스택(실행 콘텍스트 스택)으로 관리한다.
실행 컨텍스트의 구성요소 중 code evaluation state는 말 그대로 실행 콘텍스트의 코드를 실행, 중단, 재개하는데 필요한 모든 상태이다. 전역 실행 콘텍스트에서 관련 없는 실행 가능한 코드 영역을 만나면 전역 코드의 실행을 중단한 후,
새로운 실행 컨텍스트를 생성하여 코드를 실행한다.
흔히 자바스크립트는 싱글 스레드라고 하는 이유가 여기서 나온다.
여러 실행 콘텍스트의 집합에서 실제 코드를 실행하는 agent는 실행 콘텍스트를 하나밖에 실행하지 못하기 때문에
실행 코드 영역이 달라지면 실행 중인 실행 콘텍스트를 중단한 후 새로운 실행 콘텍스트를 스택에 쌓아 실행한다.
실행 컨텍스트의 스택은 하나씩 실행되는 실행 콘텍스트를 tracking 하기 위해서 사용된다. 스택 자료구조의 특성으로 현재 실행 중인 콘텍스트는 맨 위에 위치한다.
실행 컨텍스트의 생성 및 과정
초기 전역 코드가 평가되어 실행 콘텍스트에 진입하기 이전에 유일한 전역 객체 (Global Object)가 생성된다. 이 전역 객체는 단일 사본으로 존재하며 이 객체의 프로퍼티는 어떠한 곳에서도 접근할 수 있다. 초기 상태의 전역 객체에는 빌트인 객체와 BOM, DOM, 호스트 객체, 전역 함수를 포함한다.
전역 코드를 평가
- 전역 실행 컨텍스트 생성
- 전역 렉시컬 환경 생성
- 외부 렉시컬 환경 참조
- 전역 환경 레코드의 [[GlobalThisValue]]에 this 바인딩
- 전역 환경 레코드
- 객체 환경 레코드, 선언 환경 레코드 생성
실행 컨텍스트가 생성되면 자바스크립트 엔진은 실행에 필요한 여러 정보들을 담을 객체를 생성한다. 이를 Variable Object라고 한다. 이 변수 객체는 코드가 실행될 때 엔진에 의해 참조되며 코드에서는 접근할 수 없다.
- 변수
- 매개변수( parameter )와 인수 정보 ( arguments )
- 함수 선언
Variable Object는 실행 콘텍스트의 프로퍼티이기 때문에 값을 갖는데 이 값은 다른 객체를 가리킨다. 그런데 전역 코드 실행 시 생성되는 전역 콘텍스트와 함수를 실행할 때 생성되는 함수 콘텍스트의 경우, 가리키는 객체가 다르다. 이는 전역 코드와 함수의 내용이 다르기 때문이다.
전역 컨텍스트의 경우 Variable Object는 유일하며 최상위에 위치하고 모든 전역 변수, 전역 함수를 포함하는 전역 객체 (Global Object)를 가리킨다. 전역 객체는 전역에 선언된 전역 변수와 전역 함수를 프로퍼티로 소유한다.
함수 컨텍스트의 경우 Variable Object는 Activation Object(활성 객체)를 가리키며 매개변수와 인수들의 정보를 배열의 형태로 담고 있는 객체인 arguments object가 추가된다.
중간 정리
자바스크립트의 실행 콘텍스트는 추상적 개념이다. 자바스크립트의 코어 스펙이기도 하며, 자바스크립트 엔진이 구현해야 하는 자바스크립트의 개념으로 보인다. 코드가 실행되기 위한 환경정보를 물리적으로 객체 형태로 제공한다.
성격이 다른 코드들을 각각의 별도의 실행 콘텍스트로 인식하며, 각 실행 콘텍스트에서 물리적으로 생성된 객체들끼리 정보 공유를 통해 전체 코드를 실행한다. 이 부분에서 자바스크립트만의 다양한 특징이 존재한다.
자바스크립트의 콜 스택은 자료구조 특징으로 Last In First Out을 발생시키기 때문에 실행 콘텍스트들은 싱글 스레드라고 할 수 있다.
Scope
모든 프로그래밍 언어에서는 자기만의 명세로 스코프를 정의한다. 메모리 주소 값으로 우리는 프로그래밍을 할 수 없다. 각 식별자에 해당 값을 표현하는 적절한 변수명을 부여한다.
하지만 대규모 애플리케이션 같은 경우 변수 이름의 충돌 문제는 반드시 해결해야 하는 문제 중 하나이기 때문에 각 언어마다 스코프라는 규칙을 만들어 언어의 명세로 자리매김하였다.
스코프, 유효 범위는 참조 대상 식별자를 찾기 위한 규칙이다. 자바스크립트에서 스코프를 구분하면 3가지로 구분할 수 있다.
- Global Scope
- Function-level Scope
- Block-level Scope
변수는 선언 위치에 의해 스코프를 가지게 된다.
예를 들어 전역에서 선언된 변수는 전역 스코프를 갖는 전역 변수, 함수 레벨에서 선언된 변수는 함수 레벨 스코프를, 블록 내에 선언된 변수(let, const로 선언)들은 블록 레벨 스코프 변수이다.
전역 스코프를 이해하는 것은 어렵지 않을 것이다. 하지만 나는 함수 레벨과 블록 레벨의 스코프라는 것이 사실 가장 헷갈렸다. 타 언어 경우 대부분 블록 스코프를 지원했지만, 자바스크립트는 ES6에 블록 스코프를 지원하는 키워드가 추가되었다.
1. function-level scope
function foo() { //함수 선언문
if(true) {
var x ='output';
}
console.log(x); // output
}
자바스크립트보다 C-family언어를 공부하셨던 사람은 이해하기 힘들다. if문 내부 선언된 변수가 외부에서 참조된다는 것부터가 이해하기 힘들 것이다.
만약 var x 가 블록 레벨의 스코프였다면 어떻게 될까? if문이 끝날 때 해당 변수가 해제되고 콘솔에는 참조 에러를 반환할 것이다.
그렇지만 var x는 함수 레벨의 스코프이기 때문에 foo() 함수 내부 어디에서나 참조 에러 없이 참조할 수 있다.
더 또한 함수 내부의 변수가 블록 내부 변수의 값에 대해서 재할 당도 가능하다는 것이다.
정말 놀랍다기 보단 자유분방한 자바스크립을 보고 무서워졌다. 그래서 초기 개발 공부 단계에서 let과 const를 무조건 사용하라는 말이 이제야 납득과 이해가 간다.
이것이 자바스크립트의 함수 레벨 스코프이다.
2. Block-level scope
ES6에서는 타 언어와 같이 블록 레벨 스코프를 가지게 할 수 있는 키워드인 let, const가 추가되었다.
let y = 0;
{
let y = 1;
console.log(y);
}
console.log(y);
let, const는 블록 레벨 스코프를 만들어 준다. 실제 블록 내부에서만 참조가 가능하며, 외부에서 참조할 수는 없다.
ES6가 표준화되면서, 블록 레벨, 함수 레벨을 모두 지원하게 되었다. var, let, const가 서로 다르기에 필요한 상황에 알맞게 사용할 줄 알아야 한다.
다음과 같은 예제를 보자 함수 내에 존재하는 내부 함수의 경우에는 어떻게 참조가 이루어 질까?
var x = 'global';
function foo() {
var x = 'function';
console.log(x); // function
function bar() {
console.log(x); // function;
}
bar();
}
foo();
console.log(x); // global;
내부 함수는 자신을 포함하고 있는 외부 함수의 변수에 접근할 수 있다. 클로저에서와 같이 내부 함수가 더 오래 생존하는 경우, 다른 언어와 다른 메커니즘을 보여준다.
함수 bar에서 참조하는 변수 x는 함수 foo()에서 선언된 지역변수를 참조하는데, 이는 실행 콘텍스트의 스코프 체인에 의해 참조 순위에서 전역 변수의 x의 순위가 밀렸기 때문이다.
Lexical scope
렉시컬 스코프는 소스코드가 작성된 그 문맥에서 결정된다. 이 말은 함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정된다. 자바스크립트는 렉시컬 스코프를 따르기 때문에 함수를 선언한 시점에 상위 스코프가 결정된다. 함수를 어디에서 호출하였는지는 스코프 결정에 아무런 의미를 주지 않는다.
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
foo(), bar() 모두 전역 환경에 선언된 함수 선언문이다. bar가 foo함수 내부에서 호출되었다 하더라도 자바스크립트는 실행 위치가 아닌 작성된 문맥에서 스코프가 정해지기 때문에 전역 변수 x의 값을 콘솔 로그에 2번 출력할 것이다.
이러한 자바스크립트의 스코프는 ECMAScript 언어 명세에서 렉시컬 환경(Lexical environment)과 환경 레코드 (Environment Record)라는 개념으로 정의되었다.
(혼동 주의)스코프 체인은 식별자 중에서 객체의 프로퍼티가 아닌 식별자 즉 변수를 검색하는 메커니즘이고, 식별자 중에서 변수가 아닌 객체의 프로퍼티를 찾기 위해 상위로 검색하는 메커니즘은 프로토타입 체인이다.
렉시컬 환경은 식별자의 값을 관리하고, 외부에 존재하는 상위 스코프에 대한 참조를 기록하는 데 사용되는 자료구조로 환경 레코드와 외부 렉시컬 환경 참조에 대한 정보로 구성되어 있다.
ECMAScript에서 환경 레코드는 렉시컬 중첩 구조 기반으로 변수, 함수, 식별자와 값을 관리하는 데 사용된다. 함수 선언, 블록 문, try/catch 절 등의 구문이 평가될 때마다 해당 코드로 생성된 식별자와 값을 연결하는 식별자 바인딩을 기록하기 위해 새 환경 레코드가 생성된다.
실행 코드가 평가될 때마다 렉시컬 환경은 실행 콘텍스트를 구성하는 변수 환경 컴포넌트를 위해 새롭게 생성된다. 환경 레코드는 렉시컬 환경을 구성하는 컴포넌트 중 일부이며, 환경 레코드는 이 렉시컬 중첩 구조에서 외부 환경 레코드를 참조하기 위한 [[OuterEnv]] 필드를 사용한다.
외부 환경 레코드는 여러 개의 내부 환경 레코드로부터 참조될 수 있다. 함수 선언에 두 개의 중첩 함수이 포함된 경우 각 중첩 함수의 환경 레코드는 자신이 정의된 함수의 환경 레코드를 외부 환경 레코드로 한다.
bar의 렉시컬 환경에서 변수를 찾아보고, 없다면 바깥 렉시컬 환경을 참조하여 찾아보는 식으로 중첩 스코프가 가능해진다. 이 중첩 스코프 탐색은 해당하는 이름을 찾거나 바깥 렉시컬 환경 참조가 null이 될 때 탐색을 중단한다.
참고: ECMA-262 Edition3을 보면 자바스크립트의 스코프적 특징은 Scope chain(=list)과 Activation Object 등의 개념으로 설명하였다. 그리고 이 설명들이 전반적으로 널리 알려졌지만, 이다음 명세인 ECMA262 Edition5부터는 Lexical Environment와 Environment Record의 개념으로 스코프를 설명하고 있다.
중간 정리
- 자바스크립트의 스코프의 특징은 렉시컬 스코프와 함수 레벨 스코프 (ES6 + 블록 레벨 스코프)로 구성되었다.
- Lexcical Environment는 실행 콘텍스트를 구성하는 변수 환경 컴포넌트를 위해 생성된다.
- 환경 레코드는 렉시컬 환경을 구성하는 하나의 컴포넌트로 변수, 함수, 식별자의 값을 관리하기 위해 사용되며 새로운 구문이 평가될 때마다 새롭게 만들어진다.
- Lexical Environment에는 상위 렉시컬 환경(Outer Environment)을 가지고 있다. 현재 환경 레코드에서 변수를 찾고 없다면 상위 렉시컬 환경을 참조하여 찾아보는 식으로 중첩 스코프가 가능하다.
실행 콘텍스트 생성과정
1. 실행 콘텍스트에 진입하기 전 유일한 전역 객체가 생성되며, 빌트인 객체들이 설정되어 있다.
2. 전역 객체 생성 후 전역 실행 콘텍스트가 생성되고 콜 스택에 쌓인다.
3. 스코프 체인의 생성과 초기화가 이루어진다.
4. Variable Instantiation (변수 객체화) 실행
변수 객체화는 다음과 같은 순서를 따른다.
4.1 (Function Code인 경우) 매개변수(parameter)가 Variable Object의 프로퍼티로, 인수(argument)가 값으로 설정된다.
4.2 대상 코드 내의 함수 선언(함수 표현식 제외)을 대상으로 함수명이 Variable Object의 프로퍼티로, 생성된 함수 객체가 값으로 설정된다.(함수 호이 스팅)
함수 선언식은 함수명을 프로퍼티에 추가하고, 함수 객체를 즉시 할당하기 때문에 선언문 이전에 함수 호이 스팅에 의해 함수를 호출할 수 있지만 함수 표현식은 일반 변수의 방식을 따르기 때문에 선언 이전 호출이 불가능하다.
함수 선언은 선언된 함수명이 VO/GO의 프로퍼티로, 생성된 함수 객체가 값으로 설정된다. 생성된 함수 객체는 [[Scopes]] 프로퍼티를 가지게 된다. [[Scopes]] 프로퍼티는 함수 객체만이 소유하는 내부 프로퍼티로서 함수 객체가 실행되는 환경을 가리킨다. 따라서 현재 실행 콘텍스트의 스코프 체인이 참조하고 있는 객체를 값으로 설정한다.
내부 함수의 [[Scopes]] 프로퍼티는 자신의 Lexical environment와 자신을 포함하는 Outer Enviroment와 전역 객체를 가리키는데, 이때 자신을 포함하는 외부 함수의 실행 콘텍스트가 소멸하여도 [[Scopes]] 프로퍼티가 가리키는 외부 함수의 실행환경은 소멸하지 않고 참조할 수 있다. 이것을 클로저라고 한다.
4.3 대상 코드 내의 변수 선언을 대상으로 변수명이 Variable Object의 프로퍼티로, undefined가 값으로 설정된다.(변수 호이 스팅)
var 키워드로 선언된 변수는 선언 단계와 초기화 단계가 한 번에 이루어진다. 다시 말해 스코프 체인이 가리키는 변수 객체에 변수가 등록되고 변수는 undefined로 초기화된다. 따라서 변수 선언문 이전에 변수에 접근하여도 Variable Object에 변수가 존재하기 때문에 에러가 발생하지 않는다. 다만 undefined를 반환한다. 이러한 현상을 변수 호이 스팅(Variable Hoisting)이라 한다.
그와 반대로 let, const를 사용하면 한 번에 이루어지지 않는다. 메모리에 해당 식별자만 등록해 두긴 하지만 값을 초기화하진 않는 특성이 있어 선언 이전에 참조 시 참조 에러가 발생합니다.
5. this value 결정
변수 선언 처리가 끝나면 다음은 this value가 결정된다. this value가 결정되기 이전에 this는 전역 객체를 가리키고 있다가 함수 호출 패턴에 의해 this에 할당되는 값이 결정된다. 전역 코드의 경우, this는 전역 객체를 가리킨다.
전역 콘텍스트(전역 코드)의 경우, Variable Object, 스코프 체인, this 값은 언제나 전역 객체이다.
'JavaScript' 카테고리의 다른 글
export, import (0) | 2021.11.28 |
---|---|
Prototype (0) | 2021.11.28 |
Number Property (0) | 2021.11.22 |
구조 분해 할당 (0) | 2021.10.23 |
JS module (0) | 2021.10.23 |
댓글