자바스크립트는 유독 밈이 많은 개발 언어이기도 하다.
다른 언어에 비해 다소 난감한 결과들을 우리에게 표출하곤 해서 그러한 밈들이 많이 생겨난 듯하다.
그렇다면 왜 자바스크립트가 독특한 결과물을 내는지 이해해보는 건 어떨까?
여기 실행 컨텍스트라는 개념이 있다.
실행 컨텍스트는 자바스크립트의 구동원리를 이해하는데 있어서 근간이 되는 개념이다.
오늘 이 개념을 공부해보면서 다소 난감한 자바스크립트의 비밀을 조금은 이해해보도록 하자.
그렇다면, 이 실행 컨텍스트라는 것은 무엇일까? context라는 단어가 가진 뜻 그대로, 자바스크립트의 요소들이 실행되는 환경 자체를 의미한다.
여기서 코드들은 '콜스택' 이라는 실행컨텍스트 스택(stack)을 통해 관리되고, 스코프,this,식별자와 같은 요소들은 렉시컬 환경을 기반으로 관리된다.
처음 엔진이 자바스크립트 코드가 실행하기 전에 생성되는 환경이다. 이름에서 알 수 있듯이 전역적으로 관리되는 값들(함수, 모듈에 포함되어있지 않은 값)이 실행되는 환경이다.
함수를 호출할 때마다 생성되는 실행 환경이다. 실행 이전에 생성되는 전역 실행환경과 다르게, 함수 실행환경은 함수가 호출될 때 생성된다.
그 외 Eval Execution Context와 Module Execution Context 등이 있지만, 많이 언급되는 개념은 아니라서 이번에는 넘어가도록 한다.
앞서 언급했다시피, 실행 컨텍스트는 실행 컨텍스트 스택에 의해 코드가 실행된다.
스택은 자료구조의 일종으로, 후입 선출(LIFO; Last-In, First-Out) 의 로직을 갖고 있다. 쉽게 생각하면 회전초밥집에서 초밥을 다 먹고 쌓아놓은 접시들 같은 구조인데, 쌓은 접시 수를 하나씩 줄이려면 맨 아래 처음 둔 접시가 아닌 맨 위에 둔 접시부터 빼야되는 것과 동일한 로직이다. 자바스크립트도 코드를 실행할 때 이 실행 컨텍스트 스택에 추가하고 빼면서 코드의 순서를 관리한다.
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
그러면 이제, 예시코드를 보면서 어떻게 실행 컨텍스트가 생성되고 어떤 방식으로 콜스택에 의해 코드가 실행되는지 따라가보도록 하자.
실행 컨텍스트 Stack |
---|
- |
- |
- |
전역 실행 컨텍스트 (GEC) |
먼저, 해당 코드가 브라우저에 로딩 됨에 따라 전역에 대한 실행 컨텍스트가 생성되어 스택에 추가된다. 여기서 전역 스코프에 선언된 변수와, 함수 등의 정의가 진행된다.
전역적으로 코드가 하나씩 실행된다.
실행 컨텍스트 Stack |
---|
- |
- |
first() 실행 컨텍스트 |
전역 실행 컨텍스트 (GEC) |
그러다가 함수 first가 호출되는 시점에, 함수 first의 실행 컨텍스트가 생성되어 스택에 추가된다. 잠시 GEC의 실행은 멈추고, first 함수 안에 있는 log 메서드와 second 함수의 평가와 실행이 이뤄진다.
실행 컨텍스트 Stack |
---|
- |
second() 실행 컨텍스트 |
first() 실행 컨텍스트 |
전역 실행 컨텍스트 (GEC) |
함수 first 내에서 함수 second의 호출이 이뤄졌으므로, 함수 second의 실행 컨텍스트도 생성되고 스택에 추가된다. 함수의 second의 실행이 이뤄진다.
실행 컨텍스트 Stack |
---|
- |
- |
first() 실행 컨텍스트 |
전역 실행 컨텍스트 (GEC) |
스택 제일 위에 있는 함수 second의 동작을 완료하고 스택에서 삭제한다. 함수 first의 남은 실행 컨텍스트 실행을 재개한다.
실행 컨텍스트 Stack |
---|
- |
- |
- |
전역 실행 컨텍스트 (GEC) |
뒤이어 제일 위에 있던 함수 first의 동작을 완료하고 스택에서 삭제한다.
실행 컨텍스트 Stack |
---|
- |
- |
- |
- |
나머지 전역에 있는 코드를 모두 실행하고 나면 실행 컨텍스트 스택에서 GEC를 삭제하고 빈 상태로 남게 된다.
실행 컨텍스트 스택을 한 장면씩 살펴보면서, 어떤 식으로 예시 코드가 실행되는지 알아보았다. 중요한 것은 함수 실행컨텍스트는 선언이 아니라 호출되는 시점에 stack에 추가된다는 것을 잘 기억해야 한다.
앞서서는 실행 컨텍스트 스택의 동작 방식에 대해서 알아보았다.
그렇다면 각각의 실행 컨텍스트는 어떤 식으로 생성되고 실행되는 걸까?
우선, 모든 실행 컨텍스트는 평가와 실행 이 두 단계를 거쳐서 생성되고 실행된다.
평가 단계에서는 실행 컨텍스트에 있는 코드들을 분석/평가하여, 후술할 실행 컨텍스트의 구조들을 생성한다. 평가 과정에서 변수, 함수 등의 선언문만 먼저 실행하고 생성된 변수나 식별자를 key로 하여, 스코프에 등록한다. 이 과정에서, 흔히 이야기되는 호이스팅이라는 개념이 나오게 되는데, 식별자들을 실행 단계에서 사용되기 위해 평가 단계에서 구조를 생성할 때 미리 등록해놓는 절차가 이뤄진다.
실행 단계는 말그대로 코드가 실행되는 과정이다. 소스코드 실행을 위해 앞서 평가단계에서 등록된 정보(변수 등)를 실행 컨텍스트 내부에서 찾아 실행한다.
실행이 완료되고 그 결과로 변수값이 변경되는 등의 사항이 발생하면 다시 평가단계에서 했던 것 처럼 실행 컨텍스트 내부에 새롭게 반영하여 등록한다. (어디에 어떤 방식으로 반영하는지는 후술할 실행 컨텍스트의 구조를 쪼개어보면서 알아보도록 한다.)
실행 컨텍스트는 렉시컬 환경이라는 객체 데이터를 참조하고 있다. 여기서, 참조를 강조하는 이유는, 실행 컨텍스트가 제거되었다고 해서 렉시컬 환경 객체도 사라지는 것이 아니라, 별개로서 존재하는 객체라는 점을 강조하고 싶었다. (렉시컬 환경이 사라지는 시점은, 외부 어느 곳에서도 참조되지 않는 시점이며, 그 시점에 가비지컬렉터에 의해 사라진다.)
렉시컬 환경이란, 자바스크립트에서 스코프(실행 컨텍스트가 유효한 범위)를 관리하는 역할로, 변수, 함수, 객체 등이 생성될 때 식별자와 값을 저장한다.
이 렉시컬 환경 객체는 아래와 같은 3가지 정보를 갖고 있다.
변수 환경은 변수의 식별자와 값을 저장하고 관리하는 내부 컴포넌트이다.
환경기록, 외부환경참조, this 바인딩 등 렉시컬 환경과 유사한 정보를 갖고 있으나, 렉시컬 환경은 변수 let
, const
와 함수 등에 대한 정보를 모두 저장하나 변수 환경은 var
변수만 저장한다.
자바스크립트가 실행되면서 전역 객체가 생성되는데, 이 전역 객체의 실행 컨텍스트도 마찬가지로 생성된다.
모든 실행 컨텍스트가 그러하듯이, 전역실행 컨텍스트도 내부에 렉시컬 환경이 생성되는데, 이를 GlobalLexicalEnvironment(전역 렉시컬 환경)라고 한다.
const global = {
// 전역 객체
console: {
log(){}
},
};
// 전역 실행 컨텍스트
const GlobalExecutionContext = {
// 전역 렉시컬 환경
GlobalLexicalEnvironment: {
// 전역 환경 기록
GlobalEnvironmentRecord: {
// 객체 환경 기록
ObjectEnvironmentRecord: {
BindingObject: global,
},
// 선언적 환경 기록
DeclarativeEnvironmentRecord: {
// let, const로 선언한 식별자와 변수들을 저장
},
},
},
};
전역 렉시컬 환경은 환경 기록 내 ObjectEnvironmentRecord(객체 환경 기록)와 DeclarativeEnvironmentRecord(선언적 환경 기록)가 있는데,
DeclarativeEnvironmentRecord가 let, const로 선언한 변수들을 관리하고 ObjectEnvironmentRecord가 그 외 var, 전역 함수, 빌트인 프로퍼티, 빌트인 전역 함수 등을 관리한다.
전역 코드의 순서는 다음과 같이 평가된다.
DeclarativeEnvironmentRecord의 식별자들은 선언과 동시에 초기화되지 않기 때문에, DeclarativeEnvironmentRecord에 등록이 되었더라도 해당 식별자를 통해 변수에 접근할 수 없으며 이로 인해 참조 에러를 return하는 현상이 발생한다.
반면 var 로 선언한 변수는 객체 환경 기록 항목에 포함되어 선언과 동시에 초기화되어 해당 코드가 실행되지 않은 상태라도, undefined
를 return 한다.
전반적으로 전역 실행 컨텍스트와 유사하지만, this 바인딩과 외부 렉시컬 환경에 대한 참조를 결정하는 부분에서 유의해야 하는 개념들이 있다.
외부 렉시컬 환경에 대한 참조를 결정할 때, 함수는 호출 위치가 아니라, 정의된 위치에 따라 상위 스코프가 결정된다.
var 로 선언된 변수는 함수 코드블록만 지역 스코프로 인정하여, 함수 블록이 아닌 코드 블록 안에서의 변수 선언은 전역 변수 선언과 동일하게 취급되어졌다.
하지만 let과 const로 선언된 변수들은 블록 레벨 스코프로서, 모든 코드 블록을 지역 스코프로 인정하여, 함수가 아닌 코드 블록에서도 개별의 렉시컬 환경을 생성하고 기존의 전역 렉시컬 환경과 구분되어지는 특징이 있다.
함수는 호출 방식에 따라 this가 바인딩 되는 객체가 달라진다. 호출 방식에 따른 this 바인딩 방식을 이해하여 어떤 객체가 참조되는지를 생각하면, 함수 실행 컨텍스트가 실행단계에 들어갔을 때 소스코드의 동작 결과를 정확하게 예상할 수 있을 것이다.
- 모던자바스크립트 DEEP DIVE (위키북스, 이웅모 저)
- Understanding Execution Context and Execution Stack in Javascript
🙏 본 글을 참고하여 인용할 경우, 꼭 출처 표기를 부탁드립니다.