해당 게시글은 자바스크립트 실행 컨텍스트에 대해 공부한 내용을 정리한 포스트입니다.
실행 컨텍스트와 연관된JS
의 주요 개념들에 대해서도 정리했습니다.var
를 사용한 경우는 제외했습니다.
📌 용어 정리
1. 실행 컨텍스트 ( Execution Context )
실행할 코드에 제공할 환경 정보들을 모아놓은 객체입니다.
Variable Environment
, Lexical Environment
, ThisBinding
를 갖습니다.
Variable Environment
는 현재 Lexical Environment
의 스냅샷으로 변경되지 않습니다.
Lexical Environment
는 현재 변수, 함수 등의 식별자와 외부 렉시컬 환경에 대한 참조를 갖습니다.
ThisBinding
는 일반적으로 globalThis
를 갖고, 특수한 경우에 변경됩니다. ( 일반적으로 함수의 this
가 globalThis
를 가리키는 이유 )
코드 평가 단계에서 Lexical Environment
를 생성하고 변수/함수의 이름과 메모리 공간을 지정합니다.
전역 코드를 평가하는 순간에 전역 Lexical Environment
를 생성하고, 함수 코드를 평가하는 순간에 함수 E.C
를 생성합니다.
Lexical Environment
가 생성된 이후에 snapshot
을 찍어서 Variable Environment
에 복사합니다.
Variable Environment
는 기존 상태에서 변화하지 않습니다.
이후부터 E.C
라고 부르겠습니다.
2. 렉시컬 환경 ( Lexical Environment )
자바스크립트 동작을 설명하는 데 사용하는 이론상의 객체입니다.
코드를 통해서 조작할 수 없습니다.
환경 레코드(Environment Record
)와 외부 렉시컬 환경에 대한 참조(Outer Environment Reference
)를 갖습니다.
함수를 실행하는 경우 Lexical Environment
가 생성되며 Environment Record
와 Outer Environment Reference
값이 등록됩니다.
이후부터 L.E
라고 부르겠습니다.
3. 전역 렉시컬 환경 ( Global Lexical Environment )
특수한 Lexical Environment
입니다.
환경 레코드(Environment Record
)는 globalThis
를 갖습니다. ( 확실하지 않음 )
외부 렉시컬 환경(Outer Environment Reference
)은 null
을 갖습니다.
이후부터 G.L.E
라고 부르겠습니다.
4. 환경 레코드 ( Environment Record )
현재 Lexical Environment
에 속하는 변수들이 저장되는 객체입니다.
블록내에서 let
, const
로 변수가 선언되는 경우에는 해당 Lexical Environment
의 Environment Record
의 property
로 변수가 등록됩니다.
이후부터 E.R
라고 부르겠습니다.
5. 외부 렉시컬 환경에 대한 참조( Outer Environment Reference )
Lexical Environment
의 프로퍼티이며, 외부 렉시컬 환경에 대한 참조를 갖습니다.
쉽게 말해 상위 스코프의 참조를 갖는다는 의미입니다.
추가로 아래 예시를 설명하자면 Global Lexical Environment
의 Environment Record
에 outer()
가 속하고 Outer Environment Reference
는 null
을 갖습니다.
outer()
가 실행되면 Lexical Environment
의 Environment Record
에 inner()
가 등록되고 Outer Environment Reference
가 Global Lexical Environment
를 참조합니다.
inner()
가 실행되면 Lexical Environment
의 Environment Record
에 inside()
가 등록되고 Outer Environment Reference
가 outer()
를 참조합니다.
inside()
가 실행되면 Lexical Environment
의 Outer Environment Reference
가 inner()
를 참조합니다.
- 요약
outer()
는Lexical Environment
의Outer Environment Reference
를 통해서Global Lexical Environment
를 참조inner()
는Lexical Environment
의Outer Environment Reference
를 통해서outer()
를 참조inside()
는Lexical Environment
의Outer Environment Reference
를 통해서inner()
를 참조
따라서 내부 스코프는 외부 스코프를 참조할 수 있게 됩니다.
이런 원리로 동작하는 것이 스코프 체인입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// "Global Lexical Environment"의 "Outer Environment Reference"는 "null"을 갖습니다.
const outer = () => {
// 해당 "Lexical Environment"의 "Outer Environment Reference"는 "Global Lexical Environment"를 참조합니다.
const inner = () => {
// 해당 "Lexical Environment"의 "Outer Environment Reference"는 "outer()"를 참조합니다.
const inside = () => {
// 해당 "Lexical Environment"의 "Outer Environment Reference"는 "inner()"를 참조합니다.
// 따라서 여기서 특정 식별자에 접근하면
// 1. 현재 "Lexical Environment"인 "inside()" 검색
// 2. 현재 "Lexical Environment"의 "Outer Environment Reference"인 "inner()" 검색
// 3. "inner()"의 "Outer Environment Reference"인 "outer()" 검색
// 4. "outer()"의 "Outer Environment Reference"인 "Global Lexical Environment" 검색
// 위와 같은 과정을 거치면서 식별자를 검색합니다.
};
inside();
};
inner();
};
outer();
이후부터 O.E.R
라고 부르겠습니다.
6. [[Environment]]
핵심은 함수를 호출하는 시점이 아닌 선언하는 시점에 참조 값을 갖는다는 것입니다.
[[Environment]]
는 숨김 프로퍼티로 임의로 접근할 수 없습니다.
JavaScript
의 함수는 [[Environment]]
를 갖습니다.
[[Environment]]
는 본인이 선언된 시점의 Lexical Environment
를 참조하는 값을 갖습니다.
아래의 예시를 보면 이해할 수 있을겁니다.
아래의 예시에서 func()
를 통해서 출력된 fruits
가 apple
인 이유가 바로 [[Environment]]
때문입니다.
func()
가 선언된 시점 즉, Global Lexical Environment
를 func()
의 숨김 프로퍼티인 [[Environment]]
로 갖기 때문에 fruits
에 접근하면 apple
라는 결과를 얻게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fruits = "apple";
const func = () => {
console.log(fruits);
};
const say = () => {
const fruits = "banana";
func();
};
// 출력 값은? "apple"? "banana"?
say(); // "apple"
📌 Block Level Scope
전역 코드나 함수 코드같은 경우에는 코드 평가 단계를 거쳐서 E.C
가 생성되고 그 안의 E.R
에 변수/함수가 저장됩니다.
그렇다면 if
, for
, switch~case
, while
같은 블록 레벨의 코드는 어디에 저장이 될까요?
전역/함수 코드가 실행중에 블록을 만나게 되면 Block Level Environment
를 생성합니다.
그리고 그 내부에 E.R
과 O.E.R
를 생성하고 E.R
에는 블록 내부의 변수/함수들, O.E.R
에는 외부 렉시컬 환경에 대한 참조 즉, 블록이 선언된 E.R
에 대한 참조를 갖습니다.
그리고 실행 컨텍스트 스택의 제일 위에 있는 E.C
즉, 현재 실행중인 E.C
의 L.E
를 방금 만든 Block Level Environment
로 임시 대체합니다.
따라서 새로운 E.C
를 만들지 않고 동작이 가능합니다.
( 블록이 끝나면 다시 원래 L.E
로 바꿔집니다. )
📌 호이스팅
JavaScript
코드를 실행하면 2가지 단계로 나눠서 처리됩니다.
코드 평가 단계와 코드 실행 단계로 나눠집니다.
코드 평가 단계를 통해서 선언된 변수와 함수들의 이름과 메모리 공간을 등록해둡니다.
let
과 const
의 경우에는 코드 평가 단계를 통해서 <uninitialized>
로 초기화합니다.
따라서 변수 선언 문장을 만나기전에 사용하면 즉, TDZ
에서 변수를 사용하면 에러가 발생합니다.
하지만 var
의 경우에는 코드 평가 단계에서 undefined
로 초기화하기 때문에 선언전에 사용해도 에러는 발생되지 않습니다.
1
2
3
4
5
6
7
8
9
10
// 코드 평가 단계를 통해서 "Global Lexical Environment"의 "Environment Record"에 "fruits: <uninitialized>"가 등록
// "TDZ"에서 "fruits"를 사용했기 때문에 에러 발생
console.log(fruits); // ReferenceError: fruits is not defined
let fruits; // 이 시점에 "Global Lexical Environment"의 "Environment Record"에 "fruits: undefined"로 변경
fruits = "apple"; // 이 시점에 "Global Lexical Environment"의 "Environment Record"에 "fruits: "apple"로 변경
fruits = "banana"; // 이 시점에 "Global Lexical Environment"의 "Environment Record"에 "fruits: "banana"로 변경
📌 스코프와 스코프 체인
먼저 스코프에 대해 살펴보겠습니다.
MDN
에서 스코프를 현재 실행되는 컨텍스트라고 정의합니다.
현재 변수가 선언된 환경 그리고 그 변수를 접근할 수 있는 유효 범위를 스코프라고 생각합니다
즉, L.E
를 하나의 스코프라고 볼 수 있습니다.
다음은 스코프 체인에 대해 살펴보겠습니다.
L.E
는 내부 값인 O.E.R
를 통해서 외부 렉시컬 환경에 대한 참조 기억합니다.
코드가 실행되면서 식별자를 만나는 경우 먼저 L.E
의 E.R
에서 찾습니다.
그리고 찾지 못한다면 O.E.R
를 통해 외부 L.E
의 E.R
에 접근하여 식별자를 찾는 과정을 반복합니다.
이 과정을 반복하면서 G.L.E
의 E.R
까지 식별자를 찾는 것을 스코프 체인이라고 합니다.
- 요약
- 식별자 검색 시 현재
L.E
의E.R
에서 찾습니다. - 못 찾았다면 현재
L.E
의O.E.R
을 통해서 외부L.E
의E.R
에서 찾습니다. - 찾을 때까지 2번의 과정을 반복하다가
G.L.E
의E.R
에서도 못 찾는다면 오류를 냅니다.
- 식별자 검색 시 현재
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const a = 1;
const outer = () => {
const b = 2;
const inner = () => {
const c = 3;
const inside = () => {
const d = 4;
console.log(e);
}
inside();
}
}
📌 클로저
MDN
에서는 클로저의 정의를 “함수와 함수가 선언된 어휘적 환경의 조합”이라고 합니다.
여기서 어휘적 환경이란 L.E
즉, 해당 스코프의 환경에 대한 정보를 저장한 객체입니다.
먼저 클로저의 예시부터 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const outer = () => {
// 자유 변수
let v = 10;
// 클로저
const inner = () => {
console.log(++v);
};
return inner;
}
const func = outer();
func(); // 11
func(); // 12
func(); // 13
// 예시 2
outer()(); // 11
outer()(); // 11
outer()(); // 11
위 코드를 조금 살펴보겠습니다.
outer()
는 inner()
를 반환하는 함수이며, inner()
는 outer()
에 선언된 v
를 사용합니다.
outer()
를 통해서 얻은 함수를 func
에 넣고 계속 실행하는 과정을 자세히 보면 신기한 일이 발생합니다.
일반적으로 함수에서 선언된 변수를 함수가 끝나고 나서 접근할 수 있는 방법이 없기 때문에 접근할 시도를 하지 않고 가비지 컬렉터에 의해서 해제된다고 생각합니다.
즉, outer()
가 종료된 시점에서 v
라는 변수는 접근할 수 없기 때문에 해제된다고 생각합니다.
하지만 outer()
는 특별하게 inner
라는 함수를 반환하고 inner
내부에서 외부에 선언된 변수인 v
를 사용합니다.
따라서 v
는 inner()
를 통해서 접근이 가능하고, inner()
는 func
를 통해서 접근이 가능하기 때문에 가비지 컬렉터의 해제 대상이 되지 않습니다.
따라서 outer
의 어휘적 환경인 L.E
(정확히는 L.E
의 E.R
)에서 선언된 v
는 해제되지 않고 접근이 가능합니다.
이때 v
를 자유 변수라고 하고, inner()
를 클로저라고 합니다.
추가로 예시2
를 보시면 v
가 증가하지 않고 같은 값만 출력하는 것처럼 보입니다.
곰곰히 생각해보면 outer()
가 실행되는 시점에 하나의 L.E
가 생성되고 그 내부에 E.R
에 v
가 등록됩니다.
따라서 outer()
의 실행마다 독립적인 L.E
가 생성되기 때문에 서로 다른 v
를 갖기 때문에 v
가 증가하지 않는 것처럼 보이는 것입니다.
근데 여기서 또 하나의 의문이 생길 수 있습니다.
inner()
가 실행되는 시점 즉, 코드상에서는 func()
인 시점에 inner()
의 L.E
가 생성됩니다.
inner()
의 L.E
에서 접근할 수 있는 다른 L.E
는 G.L.E
밖에 없습니다.
근데 어떻게 func()
를 통해서 v
에 접근할 수 있을까요?
바로 JavaScript
의 함수가 갖는 [[Environment]]
을 이용하기 때문입니다.
즉, 생성한 시점의 L.E
의 참조를 알기 때문에 다른 곳에서 실행해도 찾아갈 수 있습니다.
🚩 마무리
이전부터 실행 컨텍스트, 클로저, 스코프 및 스코프 체인, 호이스팅 모두가 어느 정도 연관이 있다는 것은 알고 있었습니다.
모던 자바스크립트 Deep Dive
, javascript-info
, 여러 블로그들, 유튜브를 검색하면서 원리를 이해해보고자 노력했었습니다.
항상 자세한 설명을 보면 “어휘적 환경이 어떻고, 실행 컨텍스트의 동작에 의해서 발생하는 현상이라서 실행 컨텍스트를 이해하면 된다.”
그래서 실행 컨텍스트를 공부하고 이해하려면 L.E
, O.E.R
등의 개념으로 이어졌습니다.
하지만 이 개념들이 눈에 보이는 개념도 아니고, 실제로 존재하는 것을 확인할 수 없는 이론상의 존재들이라 지금 내가 이해한 것이 맞는지.. 아니면 틀린지.. 틀리면 뭐가 잘못된건지 검증하는 방법을 몰랐고 물론 지금도 이해했다고는 생각하지만 확신할 수 없습니다.
이런 경험을 했다면 여러 신뢰할 수 있는 자료들을 비교해보면서 이 자료들이 공통으로 강조하는 말이 무엇인지 찾고 이해해보고 그게 맞는지 다시 구글링을 통해서 찾아보고 요약하고 정리해보면 어느 정도 정리가 되는 것 같습니다.
결론은 어렵고 헷갈려도 주기적으로 반복하고 공부하면 어느 순간에는 이해가 가고, 클로저든 스코프 체인이든 호이스팅이든 구분해서 개념을 공부하기 보다는 실행 컨텍스트를 공부해보면 자연스럽게 모두 공부하게 되기 때문에 실행 컨텍스트에 대해 공부하는 것이 중요하다고 느꼈습니다.
📮 레퍼런스
- 모던 자바스크립트 Deep Dive
- javascript info - closure
- MDN - closure
- poiemaweb - closure
- ZeroCho - 실행 컨텍스트