실행 컨텍스트(execution context)
1. 개요
자바스크립트를 한다면 실행 컨텍스트를 이해하는 것이 필수이다. 그만큼 자바스크립트에서 실행 컨텍스트는 아주 중요한 핵심 개념이다. 왤까? 바로 자바스크립트의 동작 원리를 담고 있기 때문이다. 이를 이해함으로서 스코프, 호이스팅, 클로저, 태스크 큐와 함께 동작하는 이벤트 핸들러와 비동기 처리의 동작 방식을 이해할 수 있다.
그러니, 꼭 시간을 내서 알고 넘어가야 할 부분이다.
2. 소스코드의 평가와 실행
자바스크립트 엔진은 소스코드를 소스코드의 평가
와 소스코드의 실행
으로 나누어 처리한다.
2-1. 소스코드의 평가
실행 컨텍스트를 생성한다.
변수, 함수 등의 선언문만 먼저 실행한다.
생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록한다.
해당 과정에서
let
,const
와var
는 다르게 동작한다.var
로 선언한 변수는 선언과 초기화(undefined)를 동시에 한다.let
,const
로 선언한 변수는 선언만 진행한다.함수 표현식도 마찬가지이다. 단, 함수 선언식은 선언과 동시에 생성된 함수 객체를 즉시 할당한다.
2-2. 소스코드의 실행(런타임의 시작)
소스코드 실행에 필요한 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색해서 취득한다.
변수 값의 변경 등 소스코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록한다.
3. 실행 컨텍스트의 스택
전역에서 함수가 실행되면 또다른 실행 컨텍스트가 전역 실행 컨텍스트 위에 새롭게 쌓이게 된다. 이렇게 차곡차곡 쌓인 실행 컨텍스트는 스택 자료구조로 관리되며 이를 실행 컨텍스트 스택(콜 스택)이라고 부른다. 새로운 실행 컨텍스트가 쌓이면 소스코드의 평가와 실행이 순차적으로 다시 일어난다.
실행 컨텍스트 스택의 역할은 코드의 실행 순서를 관리하며 실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트는 언제나 현재 실행 중인 코드의 실행 컨텍스트다.
예제를 통해 실행 컨텍스트의 스택에 대해 살펴보자.
3-1. 전역 코드의 평가와 실행
평가 - 전역 변수 x와 전역 함수 foo가 전역 실행 컨텍스트에 등록된다.
실행 - 전역 변수 x에 값(1)이 할당되고 전역 함수 foo가 호출된다.
foo가 호출됨에 따라 foo 함수 실행 컨텍스트가 생성되어 전역 실행 컨텍스트 위에 쌓이게 된다.
3-2. foo 함수 코드의 평가와 실행
평가 - foo 함수의 지역 변수 y와 중첩 함수 bar가 foo 함수 실행 컨텍스트에 등록된다.
실행 - 지역 변수 y에 값(2)이 할당되고 중첩 함수 bar가 호출된다.
bar가 호출됨에 따라 bar 함수 실행 컨텍스트가 생성되어 foo 함수 실행 컨텍스트 위에 쌓이게 된다.
3-3. bar 함수 코드의 평가와 실행
평가 - bar 함수의 지역 변수 z가 bar 함수 실행 컨텍스트에 등록된다.
실행 - 지역 변수 z에 값(3)이 할당 되고 console.log 메서드를 호출한다.
이후 bar 함수는 자신의 역할을 모두 끝냈으므로 종료된다.
3-4. foo 함수 코드로 복귀
자바스크립트 엔진은 bar 함수 실행 컨텍스트를 실행 컨텍스트 스택에서 pop하여 제거한다. 이후 foo 함수는 더 이상 실행할 코드가 없으므로 종료된다.
3-5. 전역 코드로 복귀
자바스크립트 엔진은 foo 함수 실행 컨텍스트를 실행 컨텍스트 스택에서 pop하여 제거한다. 이후 더 이상 실행할 전역 코드가 남아 있지 않으므로 전역 실행 컨텍스트도 실행 컨텍스트 스택에서 pop된다.
다음의 위의 과정을 그림으로 나타난 것이다.
4. 렉시컬 환경
렉시컬 환경은 실행 컨텍스트를 구성하는 컴포넌트로 다음과 같은 것들을 기록한다.
식별자와 식별자에 바인딩된 값 - 환경 레코드(Environment Record)
상위 스코프에 대한 참조 - 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference)
다시 정리하자면, 렉시컬 환경은 스코프를 구분하여 식별자를 등록하고 관리하는 저장소 역할을 하는 렉시컬 스코프의 실체다.
렉시컬 스코프(정적 스코프)는 함수가 호출된 위치가 아니라 함수가 정의된 위치에 따라 스코프가 결정되는 것을 말한다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프이다. 반대로 함수의 호출된 위치에 따라 스코프가 결정되는 것을 동적 스코프라고 한다.
아래는 예시 코드입니다.
아래의 예시 코드를 통해 실행 컨텍스트의 생성과 식별자 검색 과정에 대해 살펴봅시다
아래에서 다룰 내용은 소스 코드의 평가만 다룬다. 즉, 실행은 다루지 않는다. 어떻게 실행 컨텍스트가 생성되는지, 스코프가 어떻게 결정되는지 위주로 정리한다.
5. 전역 렉시컬 환경의 생성
전역 실행 컨텍스트가 생성된 이후 전역 렉시컬 환경이 생성된다. 이후 전역 실행 컨텍스트에 바인딩한다. 전역 렉시컬 환경 생성은 다음과 같이 2가지로 이루어진다.
전역 환경 레코드 생성
객체 환경 레코드 생성
선언적 환경 레코드 생성
this 바인딩
하나하나 살펴보자. 참고로 전역 렉시컬 환경은 스코프 체인의 종점에 존재하기 때문에 외부 렉시컬 환경에 대한 참조
는 없다. 때문에 null
이 할당된다.
5-1. 전역 환경 레코드 생성 - 객체 환경 레코드 생성
객체 환경 레코드는 BindingObject
라고 부르는 객체와 연결된다.
BindingObject는 전역 객체로 전역 코드가 평가되기 이전에 생성된다. 빌트인 전역 프로퍼티와 빌트인 전역 함수, 그리고 표준 빌트인 객체가 추가된다. 동작 환경에 따라 클라이언트 사이드 Web API를 포함한다. 전역 객체도 Object.prototype을 상속받는다. 우리가 흔히 알고 있는 window
객체 환경 레코드를 생성하는 과정에서 다음과 같은 것들이 평가된다.
var 키워드로 선언한 전역 변수
함수 선언문으로 정의된 전역 함수
이들은 전역 객체의 프로퍼티와 메서드가 된다.
주의! 현재 시점은 평가하는 시점이다. 그러므로 선언과 초기화가 동시에 진행되지만 아직 할당은 되지 않는 상태이다. 단, 함수 선언문으로 정의된 전역 함수는 객체를 즉시 할당한다.
5-2. 전역 환경 레코드 생성 - 선언적 환경 레코드 생성
선언적 환경 레코드를 생성하는 과정에서는 다음과 같은 것들이 평가된다.
let, const 키워드로 선언한 전역 변수
let, const 키워드로 선언한 변수에 할당한 함수 표현식
위 예제에서 전역 변수 y는 let, const 키워드로 선언한 변수이므로 전역 객체의 프로퍼티가 되지 않는다. 때문에 window.y
와 같이 전역 객체의 프로퍼티로서 참조할 수 없다. 또한 let, const로 키워드로 선언한 변수는 코드 평가 단계에서 선언만 진행되기 때문에 초기화를 하기 전 접근을 한다면 참조 에러가 발생한다.
let, const 키워드로 선언한 변수도 변수 호이스팅이 발생한다. 하지만 런타임에 컨트롤이 변수 선언문(초기화하는 과정)에 도달하기 전까지 일시적 사각지대(Temporal Dead Zone: TDZ)
에 빠지기 때문에 참조할 수 없다.
5-3. this 바인딩
this 바인딩은 다음과 같은 환경 레코드에만 존재한다.
전역 환경 레코드
함수 환경 레코드
즉, 바로 위에서 다루었던 객체 환경 레코드와 선언적 환경 레코드에는 this 바인딩이 없다.
전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this가 바인딩 된다. 때문에 전역 코드에서 this를 참조하면 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 바인됭되어 있는 객체가 반환된다.
5-4. 전역 렉시컬 환경 완성!
처음 설명한 외부 렉시컬 환경에 대한 참조까지 마무리 되면 전역 렉시컬 환경이 완성된다.
이후 전역 코드가 실행되면 x에 1이 할당되고 y는 초기화 과정을 거쳐 2가 할당된다.
6. foo 함수 실행 컨텍스트 생성
foo 함수가 호출되면 foo 함수를 위한 함수 실행 컨텍스트가 생성된다. 이 함수 실행 컨텍스트는 전역 실행 컨텍스트 위에 쌓이게 된다. 이후 다음과 같은 순서로 코드 평가가 진행된다.
foo 함수 렉시컬 환경을 생성하고 foo 함수 실행 컨텍스트에 바인딩한다.
함수 환경 레코드 생성
this 바인딩
외부 렉시컬 환경에 대한 참조 결정
2~4과정을 살펴보자.
6-1. 함수 환경 레코드 생성
함수 환경 레코드는 다음과 같은 것들을 등록하고 관리한다.
매개변수
arguments 객체
함수 내부에서 선언한 지역 변수
함수 내부에서 선언한 중첩 함수
6-2. this 바인딩
함수 환경 레코드의 [[ThisValue]] 내부 슬롯에 this가 바인딩 된다. foo 함수는 일반 함수로 호출되었으므로 this는 전역 객체를 가리킨다.
6-3. 외부 렉시컬 환경에 대한 참조 결정
foo 함수 정의는 전역 코드 평가 시점에 평가된다. 즉, foo 함수의 상위 스코프는 전역 스코프이다. 때문에 외부 렉시컬 환경에 대한 참조에 전역 렉시켤 환경의 참조가 할당된다.
6-4. foo 함수 렉시컬 환경 완성!
완성된 foo 함수 렉키컬 환경은 다음과 같다.
이후 foo 함수 코드가 실행이 되면 변수에 값이 할당된다.
7. 그 이후, 그리고 실행 컨텍스트와 블록 레벨 스코프
bar 함수 렉시컬 환경은 foo 함수 렉시컬 환경과 비슷하게 만들어진다. 단, 외부 렉시컬 환경에 대한 참조에 대해선 항상 조심해야 한다. 호출 시점이 아니라 정의된 시점을 잊지 말아야 한다. 예시에서는 foo 함수 내부에서 bar 함수가 정의되어 있기 때문에 상위 렉시컬 환경이 foo 함수의 렉시컬 환경이다. 하지만 bar 함수가 전역에서 정의되었다면 bar 함수의 외부 렉시컬 환경에 대한 참조는 전역 렉시컬 환경이다. 이를 조심하도록 하자.
실행 컨텍스트 스택이 쌓이고 함수가 종료되면 스택에서 pop된다. 이때 함수 렉시컬 환경도 함께 소멸된다. 하지만 렉시컬 환경이 누군가 참조하고 있다면 해당 렉시컬 환경은 소멸하지 않는다. 이는 클로저를 이해하는 포인트가 될 것이다. 자세한 내용은 클로저 파트를 참고 바란다.
let, const는 블록 레벨 스코프를 가진다. 예를 들어, if문을 만나면 블록 렉시컬 환경을 만들게 된다. 만약 if 문의 코드 블록 내에서 정의된 함수가 있다면 이 함수의 상위 스코프는 if 문의 코드 블록이 생성한 렉시컬 환경이다.
for 문은 좀더 눈여겨 봐야한다. foo 문의 코드 블록이 반복해서 실행될 때마다 식별자의 값을 유지 해야하기 때문에 for 문의 코드 블록이 반복해서 실행될 때마다 독립적인 렉시컬 환경을 생성하여 식별자의 값을 유지한다.
8. Conclusion
지금까지 미뤘던 실행 컨텍스트에 대해 정리를 해보았다. 아직 마지막 부분이 부족하다. 스스로 그림도 그려보면서 해야 더 이해학 잘 될 것 같은데... 실행 컨텍스트 스택도 이해가 되었다. 지난 우테코에서 콜 스택 문제를 풀었던 경험이 떠올랐다. 그리고 저번에 정리한 클로저가 조금 더 이해가 되었다. 이걸 먼저 정리를 하면서 공부를 했어야 했는데 🤣 아직 용어가 친숙하지 않아 버벅이는 면이 있다. 렉시컬 환경이라든지, 환경 레코드, 외부 렉시컬 환경에 의한 참조, 전역 환경 레코드, 선언적 환경 레코드 등등 비슷하지만 역할이 다른 용어들이 많고 var, let, const, 함수 선언식, 함수 표현식에 따라 변수 선언, 초기, 할당이 제각기라는 점이 흥미롭지만 복습을 하지 않으면 다시 헷갈릴 수 있는 개념이 될 수 있다. 계속해서 읽고 내것으로 만들어 보자 🔥🔥🔥
참고
도서 - 모던 자바스크립트 Deep Dive 23장 실행 컨텍스트
📅 2023-03-30
Last updated