실행 컨텍스트란?
실행 컨텍스트(excution context)는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체다. 자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행한다. 실행 컨텍스트를 살펴보기 전에 스택(stack)과 큐(queue)의 개념을 알아야 한다.
스택(Stack)
스택은 출입구가 하나뿐인 데이터 구조다. 비어 있는 스택에 순서대로 데이터 a, b, c, d를 저장했다면, 꺼낼 때는 반대로 d, c, b, a의 순서로 꺼낼 수 밖에 없다.
큐(Queue)
큐는 양쪽이 모두 열려있는 파이프를 떠올리면 된다. 종류에 따라 양쪽 모두 입력과 출력이 가능한 큐도 있으나 보통은 한쪽은 입력만, 다른 한쪽은 출력만 담당하는 구조다. 이 경우 순서대로 데이터 a, b, c, d를 저장했다면 꺼낼 때도 역시 a, b, c, d의 순서로 꺼낼 수 밖에 없다.
실행 컨텍스트는 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택에 쌓아올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다. 여기서 '동일한 관경', 즉 하나의 실행 컨텍스트를 구성할 수 있는 방법으로는 전역공간, eval() 함수, 함수 등이 있다. 자동으로 생성되는 전역공간과 악마로 취급받는 eval을 제외하면 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것뿐이다.
eval()을 사용하지 마세요
eval()은 evalute의 약자로 문자열이 코드라고 가정하고 평가해서 실행하는 함수다. 그러나 eval은 모던 프로그래밍에서 사용되지 않고 있다. 그 이유는 아래와 같다.
- eval은 여전히 느린 JS 인터프리터를 사용
- 가독성이 떨어져 유지보수가 힘들다.
- 보안에 취약
- 가비지 컬렉터에 수집되지 않는다.
- 굳이 쓸 이유가 없다.
// --------------------(1)
var a = 1;
function outer() {
function inner() {
console.log(a); // undefined
var a = 3;
}
inner(); // ------- (2)
console.log(a); // 1
}
outer(); // ------------(3)
console.log(a); // 1
위 코드를 보면 자바스크립트 코드를 실행하는 순간(1) 전역 컨텍스트가 콜 스택에 담긴다. 콜 스택에는 전역 컨텍스트 외에 다른 것이 없으므로 전역 컨텍스트와 관련된 코드들을 순차로 진행하다가 (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담는다. 이 후 전역 컨텍스트와 관련된 코드 실행을 일시중단하고 대신 otuer 실행 컨텍스트와 관련된 코드, 즉 outer 함수 내부의 코드들을 순차대로 실행한다. 다시 (2)에서 inner 함수의 실행 컨텍스트가 콜 스택의 가장 위에 담기면 otuer 컨텍스트와 관련된 코드 실행을 중단하고 Inner 함수 내부의 코드를 순서대로 진행한다.
위 코드의 실행 컨텍스트를 그림으로 보면 아래와 같다.
inner 함수 내부에서 a변수에 값 3을 할당하고 나면 inner 함수의 실행이 종료되면서 inner 실행 컨텍스트가 콜 스택에서 제거된다. 그럼 아래에 있던 outer 컨텍스트가 콜 스택의 맨 위에 존재하게 되므로 중단했던 (2)의 다음 줄부터 이어서 실행한다.
스택의 구조를 잘 보면 한 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 있다.
자바스크립트 엔진이 저장한 각 실행 컨텍스트 객체는 개발자가 코드를 통해 확인할 수는 없으나 VariableEnvironment, LexicalEnvironment, ThisBinding과 같은 정보들이 담긴다.
VariableEnvironment
VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다. 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용한다.
LexicalEnvironment
LexicalEnvironment는 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아둔 것이다.
LexicalEnvironment는 두 가지로 구성되어 있는데, Environment Record와 Outer Environment Reference(scope)이다.
environmentRecord와 호이스팅
environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등이 식별자에 해당한다. 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다.
전역 실행컨텍스트
전역 실행컨텍스트는 변수 객체를 생성하는 대신 자바스크립트 구동 환경이 별도로 제공하는 객체, 즉 전역 객체를 활용한다. 이는 브라우저의 window, Node.js의 global 객체 등이 있으며, 자바스크립트 내장 객체가 아닌 호스트 객체로 분류된다.
실행 컨텍스트가 실행되기 관여할 코드들이 실행 전의 상태임에도 불구하고 변수 정보를 수집하는 과정을 모두 마친다. 즉, 자바스크립트 엔진은 이미 해당 환경에 속한 변수명을 모두 알고 있게 된다. 이 때 발생하는 것이 바로 '호이스팅'이다. '끌어올리다'라는 의미를 가지며, 실제로 자바스크립트 엔진이 식별자들을 최상단으로 끌어올리진 않지만 편의상 끌어올린 것으로 간주하는 것이다.
호이스팅 규칙
function a (x) {
console.log(x); // (1)
var x;
console.log(x); // (2)
var x = 2;
console.log(x); // (3)
}
a(1)
위 코드를 보면 (1)은 1, (2)는 undefined, (3)은 2라고 예상하겠지만 실제로는 호이스팅이 발생해 모든 변수 선언이 위로 끌어올려져 아래와 같은 코드로 동작하게 된다.(이해를 돕기 위함이며 실제로 자바스크립트 엔진은 이러한 변환 과정을 거치지 않는다.)
* 매개변수는 함수 내부의 다른 코드보다 먼저 선언 및 할당이 이뤄진 것으로 간주할 수 있다. (var x = 1;)
function a () {
var x;
var x;
var x;
x = 1;
console.log(x); // 1
console.log(x); // 1
x = 2;
console.log(x); // 2
}
호이스팅 이후 실제 코드는 결과는 위와 같이 처음 예상했던 것과 다르게 나온다.
함수 선언을 추가하는 것 또한 마찬가지다.
function a () {
console.log(b); // (1)
var b = 'bbb';
console.log(b); // (2)
function b () {}
console.log(b); // (3)
}
a();
(1)은 undefined, (2)는 bbb, (3)은 b함수로 출력될 것이라 예상할 수 있다.
a 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성되고, 이 때 변수명과 함수 선언의 정보를 위로 끌어올린다. 변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면, 함수 선언은 함수 전체를 끌어올린다.
이해를 돕기 위해 호이스팅 된 코드로 바꿔보자.
function a () {
var b;
function b () {}
console.log(b); // b 함수
b = 'bbb';
console.log(b); // 'bbb'
console.log(b); // 'bbb'
}
예상과는 달리 위와 같은 결과가 나왔다. 이처럼 호이스팅 개념을 정확히 이해하지 못하면 결과를 예측하기 어려울 수 있다.
함수 선언문과 함수 표현식
함수 선언문은 function정의부만 존재하고 별도의 할당 명령이 없다. 반대로 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말한다. 함수 선언문의 경우 반드시 함수명이 정의돼 있어야 하는 반면, 함수 표현식은 없어도 된다. 따라서 함수 표현식은 '익명 함수'라고도 불린다.
function a () {...} // 함수 선언문. 함수명 a가 곧 변수명이다.
a();
var b = function() {...} // (익명)함수 표현식. 변수명 b가 곧 함수명이다.
b();
함수 선언문과 함수 표현식이 호이스팅에서 어떤 차이가 있는지 알아보자.
console.log(sum(1, 2));
console.log(multiply(3, 4));
function sum(a, b) { // 함수 선언식
return a + b;
}
var multiply = function(a, b) { // 함수 표현식
return a * b;
}
위 코드가 호이스팅이 발생하면서 아래와 같은 코드가 될 것이다.
function sum(a, b) { // 함수 선언식
return a + b;
}
var multiply;
console.log(sum(1, 2));
console.log(multiply(3, 4));
multiply = function(a, b) { // 함수 표현식
return a * b;
}
함수 선언문은 전체를 호이스팅한 반면에 함수 표현식은 변수 선언부만 호이스팅했다. 함수도 하나의 값으로 취급할 수 있다는 것이 바로 이런 것이다. 함수를 다른 변수에 값으로 '할당'한 것이 곧 함수 표현식이다.
sum 함수는 선언 전에 호출해도 문제 없이 실행되지만, multiply함수를 호출한 부분에서는 'multiply is not a function'이라는 에러 메시지가 출력될 것이다.
함수 호이스팅은 자바스크립트를 좁 더 쉽게 접근할 수 있게 해주는 측면도 있지만, 반대로 혼란을 일으키기도 한다. 따라서 원활한 협업을 위해 동명의 함수를 중복 선언하는 경우는 없는 것이 가장 좋으나, 여럿 존재하는 상황이라면 모든 함수가 함수 표현식으로 정의되어 있는 것이 안전하다.
스코프, 스코프 체인, outerEnvironmentReference
스코프(Scope)란 식별자에 대한 유효범위이다. 어떤 경계 A의 외부에서 선언한 변수는 A의 외부뿐 아니라 A의 내부에서도 접근이 가능하지만, A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있다. 대부분의 언어제 존재하는 개념이나 ES5까지의 자바스크립트에서는 오직 함수에 의해서만 스코프가 생성됐다. 이러한 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인(Scope chain)이라 한다. LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference는 이 스코프 체인을 가능하게 한다.
스코프 체인
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.
예를 들어 A함수 내부에 B함수 선언, B함수 내부에 C함수 선언인 상황에서 함수 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조한다. 다시 함수 B의 outerEnvironmentReference는 함수A의 LexicalEnvironment를 참조한다. 이렇게 연결리스트 형태를 띄면서 '선언 시점의 LexicalEnvironment'를 계속 찾아 올라가면 마지막에 전역 컨텍스트의 LexicalEnvironment가 있을 것이다. 이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우, 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근이 가능하게 된다.
아래 코드 흐름으로 구체적으로 알아보자.
var a = 1;
var outer = function() {
var inner = function() {
console.log(a);
var a = 3;
};
inner();
console.log(a);
}
outer();
console.log(a);
- 전역 컨텍스트의 environmentRecord에 {a, outer} 식별자가 저장된다.
- 변수 a에 1을, outer에 함수를 할당한다.
- outer 함수를 호출한다. 호출부 이후 코드가 임시중단되고, outer 실행 컨텍스트가 활성화되어 2번째 줄로 이동한다.
- outer의 실행 컨텍스트의 environmentRecord에 {inner} 식별자가 저장된다.
- outer 스코프에 있는 변수 inner에 함수를 할당한다.
- inner 함수를 호출한다. 호출부 이후 코드가 임시중단되고, inner 실행 컨텍스트가 활성화되어 3번째 줄로 이동한다.
- inner의 실행 컨텍스트의 environmentRecord에 {a} 식별자가 저장된다.
- 콘솔에 식별자 a에 접근하고자 검색을 시도한다. 발견했지만 아직 할당된 값이 없다. (undefined 출력)
- inner 스코프에 있는 변수 a에 3을 할당한다.
- inner 함수 실행이 종료되고 콜 스택에서 inner 실행 컨텍스트가 제거된다.
- outer 스코프에서 변수 a에 접근하고자 하지만 environmentRecord에 a가 없다. outerEnvironmentReference(GLOBAL)에 있는 environmentRecord로 넘어가서 a를 찾아 값 1을 출력한다.
- outer 함수 실행이 종료되고 콜 스택에서 outer 실행 컨텍스트가 제거된다.
- 마지막 줄에서 식별자 a에 접근하고자 한다. 전역 컨텍스트의 environmentRecord에서 a를 찾고 값 1을 출력한다.
현재 스코프 체인 상에 있는 변수라고 해서 무조건 접근 가능한 것은 아니다. 위 코드에서 식별자 a는 전역 공간와 inner 함수 내부에서 모두 선언되었다. 이 경우, 이미 inner 스코프의 environmentRecord에 a가 있으므로 스코프 체인 검색을 더 진행하지 않고 해당 a를 즉시 반환한다. 즉, 함수 내부에서 a 변수를 선언했기 때문에 전역 공간의 동일한 이름의 a에 접근할 수 없다. 이를 변수 은닉화라고 한다.
전역변수와 지역변수
전역 스코프에서 선언된 변수(a, outer) 전역변수이고,
함수 내부에서 선언한 변수(inner, a)는 지역변수이다.
this
실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. 실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우, this에는 전역 객체가 저장된다. 그밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다르다. 자세한 내용은 3장에서 다룬다.
Reference
'개발 > 프론트엔드' 카테고리의 다른 글
[JavaScript] 코어 자바스크립트 week4 - 콜백함수 (0) | 2024.11.11 |
---|---|
[JavaScript] 코어 자바스크립트 week3 -this (0) | 2024.11.05 |
[JavaScript] 코어 자바스크립트 week1 - 데이터 타입 (3) | 2024.10.22 |
[NextJS] GIF는 Image컴포넌트의 최적화가 지원되지 않는다?! (feat. Cloudinary) (0) | 2024.10.17 |
[CI/CD]vercel에 무료로 organization레포지토리 배포하기 (0) | 2024.09.26 |