실행 컨텍스트를 한마디로 정의할 수 있다면 좋겠지만, 실행 컨텍스트는 한 줄로 정리될만큼 단순한 개념은 아닙니다.
자바스크립트 동작원리의 핵심 개념이 바로 실행 컨텍스트이기 때문입니다.
그럼에도 불구하고 한마디로 정리해보면 다음과 같습니다.
"소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역"
조금 더 자세히 정리하면 다음과 같습니다.
"스코프(식별자 등록 및 관리하는 유효 범위)와 코드 실행 순서 관리를 구현한 내부 메커니즘"
따라서, 모든 자바스크립트 코드는 실행 컨텍스트를 통해 실행되고 관리됩니다.
또한 실행 컨텍스트를 생성하는 과정과 관리하는 내용은 코드의 종류에 따라 달라집니다.
전역 코드, 함수 코드, eval 코드, 모듈 코드 총 4가지의 코드가 존재합니다.
모든 코드는, 다음의 2가지의 과정을 거칩니다.
1. 소스코드의 평가 - 선언문들을 쭉 훑고, 변수나 함수 식별자를 key로 하여 렉시컬 환경의 환경 레코드에 저장합니다.
(렉시컬 환경의 환경 레코드 == 실행 컨텍스트가 관리하는 스코프)
2. 소스코드의 실행 - 즉, 런타임! 선언문을 제외한 코드(ex.할당문)를 실행합니다.
(이때 변수나 함수의 참조는 환경 레코드에서 검색하여 취득해옵니다.)
실행 컨텍스트의 메커니즘은 다음 2가지입니다. (이 중 실행 컨텍스트의 구성 요소라고 할 수 있는 것은, 렉시컬 환경입니다.)
렉시컬 환경 (Rexical Environment) == 스코프 |
식별자와 식별자에 바인딩 된 값, 상위 스코프에 대한 참조 관리 |
실행 컨텍스트 스택 (Call Stack) | 코드의 실행 순서 관리 (콜 스택이라고도 부름) |
실행 컨텍스트 스택
실행 컨텍스트 스택(aka 콜 스택)은 단순하게, 함수가 실행되는 예시를 생각해보면 됩니다.
함수가 실행되었을 때의 모습을 어셈블리언어로 확인해보면, 함수 공간을 위한 stack을 확보하고, 함수가 종료되면 해당 stack은 반환됩니다. 이를 위해 BP(베이스 포인터)와 SP(스택 포인터)가 사용되었고 보통 스택 포인터가 움직이면서 스택의 공간을 확보했었죠.
(C언어 이야기이긴 했는데, 이해하지 못했다면 넘어가도 좋습니다.)
다시 돌아와서, 실행 컨텍스트가 콜 스택에 쌓이는 과정은 함수를 위한 스택을 확보하는 것과 동일하게 동작을 한다고 보시면 됩니다. 해당 코드가 실행됨에 따라 해당 코드가 포함되어 있는 실행 컨텍스트가 push 되고, 해당 코드가 종료됨에 따라 해당코드가 포함되어 있는 실행 컨텍스트가 pop 됩니다. (이게 분기 처리의 방식이죠. 제어권을 넘기는 방식이기도 하고요. JMP를 통한 방식!)
참고로, 꽤 중요한 개념인데, "실행 컨텍스트는 렉시컬 환경이 완성된 이후에야 콜 스택에 push" 가 됩니다.
이 덕분에 코드가 평가되던 시점(렉시컬 환경이 구성되던 시점)에 실행 중이던 실행 컨텍스트(맨 상위의 실행 컨텍스트)를 외부 렉시컬 환경에 대한 참조로 나타낼 수 있게 되었습니다.
외부 렉시컬 환경에 대한 참조 == 상위 스코프에 대한 참조 (전역일 경우엔 null) 이므로
이를 통해 실행컨텍스트의 렉시컬 환경을 단방향으로 연결한 스코프 체인을 구성할 수 있고,
식별자 검색 시에는 스코프 체인을 따라서 상위 스코프 방향으로 검색을 할 수 있게 됩니다.
추가로, 함수의 상위 스코프는 함수 객체의 내부 슬롯인 [[ Environment ]] 에 저장됩니다.
그리고 그 [[ Environment ]] 에는 함수가 평가되던 시점의 실행 컨텍스트의 렉시컬 환경에 대한 참조가 저장됩니다.
그러다가 함수가 실행되어, 함수의 실행 컨텍스트가 구성될 때 '외부 렉시컬에 대한 참조' 에는 [[ Environment ]] 의 값을 그대로 집어 넣습니다.
참고) 렉시컬 스코프란? (렉시컬 환경과 완전 동의어는 아님에 유의!)
정적 스코프를 의미하며, 함수를 호출하는 시점(동적)이 아닌 함수가 정의된 시점(정적)에 의해 정해지는 유효 범위를 의미한다.
보통 대부분의 언어들은 렉시컬 스코프를 따른다.
렉시컬 환경
렉시컬 환경은 다음 2가지의 요소로 구성되어 있습니다.
환경 레코드 | 소스코드의 타입(4가지)에 따라 내용에 차이가 존재합니다. |
외부 렉시컬 환경에 대한 참조 | 상위 스코프에 대한 참조 |
렉시컬 환경의 구성 요소 중 하나인 환경 레코드는 코드의 타입에 따라 내용이 다르다고 했는데,
이제부터는 코드와 그림을 보면서 바로 바로 이해해봅시다!
//코드 평가 단계라고 가정
var x = 1;
const y = 2;
function foo(a) {
var x = 3;
const v = 4;
function bar(b) {
const z = 5;
console.log(a + b + x + v + z);
}
bar(10);
}
foo(20); // 아직 해당 코드는 실행 전!
해당 코드는 전역 코드라고 가정합니다. (모듈 코드가 아니라!)
현재는 코드 평가 시점입니다. 전역 코드 평가는 아래의 과정을 거쳐 진행됩니다.
1. 전역 실행 컨텍스트 생성
2. 전역 렉시컬 환경 생성
2.1. 전역 환경 레코드 생성(객체 환경 레코드, 선언적 환경 레코드, this 바인딩)
2.2. 외부 렉시컬에 대한 참조 결정
1. 전역 실행 컨텍스트 생성
전역 실행 컨텍스트는 생성과 동시에 콜 스택에 push 합니다. (보통은 렉시컬 환경을 구성한 이후에 push 합니다.)
이후, 전역 렉시컬 환경을 생성하고 이를 전역 실행 컨텍스트에 바인딩(LexiclaEnvironment에 참조값 할당) 합니다.
2. 전역 렉시컬 환경 생성 (전역 환경 레코드, 외부 렉시컬 환경에 대한 참조)
전역 렉시컬 환경의 전역 환경 레코드는 객체 환경 레코드와 선언전 환경 레코드로 구성되어 있습니다.
객체 환경 레코드 (Object Environment Record) | BindingObject 라고 부르는 객체가 존재한다.(전역 객체와 바인딩) 그 곳에 전역 객체의 프로퍼티, 메서드로 등록된다. |
선언전 환경 레코드 (Declarative Environment Record) | let, const 로 선언된 전역 변수가 등록된다. |
[[GlobalThisValue]] | this가 어느 객체를 가리킬 것인지 바인딩 |
코드 평가 시점이니, 코드를 쭉 훑으면서 선언문만 실행하면서 let, const는 선언전 환경 레코드에 <uninitialized> 상태로 등록하고,
그 외의 선언은 전역 객체 (브라우저 기준 window)의 프로퍼티 및 메서드로 등록하고, 변수는 <undefined> 값으로 초기화합니다.
또한 적절하게 this도 바인딩 하는데, this는 호출 시점에 '호출 방식'에 의해 정해집니다. 하지만 [[GlobalThisValue]] 의 경우에는 바로 전역 객체가 바인딩 됩니다.
참고로, let & const 는 TDZ에 의해 실제로 호이스팅이 발생함에도 불구하고 호이스팅이 발생하지 않는 것처럼 보이게 됩니다.
실제로 let & const는 선언과 초기화 단계가 분리되기 때문에 선언 직전에 코드에서 참조하려고 하면 참조 에러를 뱉게 됩니다.
TDZ의 간단한 원리도 이해하게 되었네요!
외부렉시컬 환경에 대한 참조 는 null 입니다. (전역이기 때문)
이제, 코드의 실행 시점입니다. 실행 시점에는 선언문을 제외한 코드를 순차적으로 실행합니다.
식별자를 검색하고, 식별자를 결정하여 적절한 값을 할당(바인딩 혹은 등록) 합니다.
그 결과 최종적으로 아래와 같은 모습을 보이게 됩니다.
전역 코드가 실행하는 것까지 위에서 지켜봤습니다. (다만, foo는 아직 실행 전이라고 가정했습니다.)
이제 foo 함수가 실행되는 경우를 봅시다. foo 함수가 실행되는 시점에야, 제어권이 foo 함수에게 넘어가고 foo 함수의 내부 코드가 평가되기 시작합니다. 평가의 과정은 위의 전역 코드 평가 과정과 동일합니다. (다만 워딩을 전역 -> 함수로만 바꿔서 이해하면 됩니다.)
다만 함수 환경 레코드는 좀 더 단순하게 구성되어 있습니다.
함수 환경 레코드 | 매개변수, arguments 객체, 지역 변수등을 등록 및 관리 |
[[ThisValue]] | this가 가리키는 객체 바인딩 |
전역 코드와는 다르게 함수 코드는 렉시컬 환경이 선언문 실행을 통해 구성된 이후에야,
함수의 실행 컨텍스트가 실행 컨텍스트에 push 된다는 점만 다릅니다.
아래는 함수 내부 코드의 선언문만 실행한 이후의 모습이며, 오른쪽은 함수 내부 코드를 전부 실행한 이후의 모습입니다.
또 중요한 개념 중 하나인데, 렉시컬 환경은 실행 컨텍스트와 독립적입니다. (그래야 클로저 동작 가능)
비록, 렉시컬 환경이 구성된 이후에야 실행 컨텍스트가 push 되는 것은 맞으나, 만약 다른 곳에서 해당 렉시컬 환경에 대한 참조가 유지된다면 메모리에서 제거되지 않습니다. 렉시컬 환경에 대한 참조가 아예 없어야만 가비지 컬렉터에 의해 회수가 됩니다!
실행 컨텍스트와 렉시컬 환경이 독립적이라는 개념이 이해가 안간다면, 위에서 설명한 그림을 다시 보세요!
(포인터의 형식으로 렉시컬 환경에 대한 참조를 유지하고 있는 것이지, 아예 실행 컨텍스트에 종속적이진 않습니다!)
비교적 긴 글을 통해 실행 컨텍스트에 대해 이해해보는 시간을 가졌습니다.
한번 명확하게 원리를 이해하고 나면, 자바스크립트의 동작 원리를 상당 부분 이해할 수 있습니다.
식별자 바인딩을 유지하는 방법 : 환경 레코드에 값 등록
호이스팅이 발생하는 이유 : 코드 평가를 먼저 하기 때문
클로저의 동작 방식 : 렉시컬 환경에 대한 참조 및 유지 (가비지 컬렉터에 의해 참조가 0일 때만 회수)
태스크 큐와 함께 동작하는 이벤트 핸들러와 비동기 처리의 방식 이해 (아직 x)
이 4가지를 이해할 수 있을만큼 아주 중요한 개념이었습니다.
긴 글 읽느라 고생하셨습니다.
참고 서적 ) 모던 자바스크립트 Deep Dive (저자-이웅모)
'Develop > Frontend' 카테고리의 다른 글
프로토타입 (ProtoType) (0) | 2025.02.11 |
---|---|
클로저 (Closure) (0) | 2025.02.06 |
zod 유효성 검사에서 겪은 트러블 슈팅 (0) | 2025.02.04 |
useCallback은 도대체 언제 사용하는게 좋을까? (0) | 2025.01.09 |
pnpm 도입기 (왜 yarn berry가 아닌가?) (0) | 2025.01.03 |