[JS] 실행 컨텍스트
1. 실행 컨택스트란?
실행 코드에 제공할 환경 정보를 모아둔 객체
동일한 환경을 단위로 실행 컨택스트가 생성되는데, 여기서 동일한 환경은 전역공간, 함수, eval()등으로 만들어 낼 수 있습니다. 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수는 없습니다. 다만, 담기는 정보에 대해서 알 수 있는데, 그 부분은 아래서에서 다루도록 하고 지금은 생성된 컨택스트가 어떻게 처리되는 지 알아보도록 하겠습니다.
1-1.콜스택
실행 컨택스트 처리 목록
생성된 컨택스트들은 차례대로 콜스택에 담깁니다.
콜스택은 작업목록으로 이해하면 쉬운데, 말 그대로 스택 구조이기 때문에 선입후출( 먼저 생성된 컨택스트가 나중에 처리되는 방식)방식으로 작동하고 처리 후 컨택스트는 작업목록에서 하나씩 사라집니다.
1-2. 예시
예를 들어 아래 코드의 경우, 코드가 실행되면 다음과 같은 순서로 콜스택을 쌓습니다.
var a = 1;
function outer() {
function inner() {
console.log(a);
var a = 3;
}
inner();
console.log(a);
}
outer();
console.log(a);
-
실행하자마자 전역 컨택스트가 생성 되고 콜스택에 들어갑니다. 참조할 외부 환경이 없기 때문에 해당 환경정보인 전역변수 a, 전역 함수 outer를 담습니다.
-
outer()를 호출하는 순간 전역 컨택스트 코드 실행을 일시정지하고 outer 내부의 환경정보를 수집해서 콜스택에 넣습니다.
-
inner()를 호출하는 순간 outer 코드 실행을 일시정지하고 inner 내부의 환경정보를 수집해서 콜스택에 넣습니다.
-
var a = 3을 마지막으로 inner()의 코드 실행이 완료되면 해당 컨택스트는 콜스택에서 제거됩니다.
-
outer도 일시정지된 이후 코드를 실행, console.log(a)이 완료되면 콜스택에서 제거됩니다.
-
전역 컨택스트 역시 일시정지된 이후의 console.log(a)를 처리하고 콜스택에서 제거됩니다.
이 때 주의점은 콜스택에 쌓여서 최상단에 위치한 순간 해당 컨택스트의 코드는 언제나 실행중이라는 점입니다. 실행하지 않고 내부함수로 들어가는 게 아니라, 실행하던걸 잠시 멈추고 내부로 들어갔다가 내부함수가 완료되면 다시 중단 지점부터 실행 후 완료를 합니다. 그리고 실행이 될 때 환경 정보를 수집해 컨택스트 객체에 저장합니다.
그럼 이제 컨택스트에 저장되는 환경 정보에 대해서 알아보도록 하겠습니다.
- VariableEvironment
- LexicalEvironment
- thisBinding
2. VariableEvironment
실행 컨택스트가 생성됐을 때 만들어진 환경 정보 원본
현재 컨텍스트 내의 식별자에 대한 정보(환경 레코드), 외부 환경 정보로 이뤄져있습니다. 선언 시점의 LexicalEnvironment 스냅샷이라고도 하는데 이유는 처음엔 LexicalEnvironment와 동일하지만, 이후 LexicalEnvironment이 코드를 실행하면서 값이 변할 때도 VariableEvironment는 원본을 유지하고 있기 때문입니다.
3. LexicalEvironment
VariableEvironment을 복사한 유동적인 환경 정보
VariableEvironment의 복사본으로 환경레코드영역과 외부참조 영역을 갖고 있습니다. 컨택스트가 환경정보를 수집하는 초기화과정에선 동일하지만 이후 코드를 실행하면서 VariableEvironment와 달리 값이 유동적으로 변합니다.
3-1. Evronment Record
호이스팅으로 식별자 정보를 수집
환경 레코드 영역엔 현재 컨택스트의 식별자 정보들을 저장합니다. 여기서 말하는 ‘식별자’란, 우리가 잘 알고 있는 변수의 이름 외에도 매개변수 식별자, 선언한 함수가 있을 경우그 함수 자체 등이 있습니다. 해당 컨택스트의 내부를 처음부터 끝까지 차례대로 수집하는 데 수집하는 동안 선언만 될 뿐 실행은 되지 않습니다. 즉, 자바스크립트 엔진은 코드를 실행하기 전에 해당 환경에 속한 코드의 식별자를 모두 알게 됩니다.
호이스팅
우리는 이런 현상을 호이스팅이라고 합니다. 실제로 자바스크립트 엔진이 변수를 코드 상단으로 끌어오는 건 아니지만, 비슷한 효과를 볼 수 있기 때문입니다. 호이스팅을 통해 받아 온 환경정보는 어떤 종류의 식별자인지 따라 다릅니다. 아래 예시를 보면서 설명하도록 하겠습니다.
console.log(a)//undefined
console.log(c)//c 함수
var a = 1;
function c() { 함수 내용 }
console.log(a)//1
console.log(c)//c 함수
먼저, 변수 식별자 a 경우 선언부만 호이스팅이 되고 환경정보에 담깁니다. 그래서 할당이 되기 전에 실행되는 console.log 결과는 undefined입니다. 반면, 함수 선언문 식별자 c는 선언부와 할당부 모두 호이스팅을 하고 환경정보에 담기 때문에 아직 함수 내용을 적지 않은 순간에도 해당 내용이 출력됩니다. 그렇다면 다음 코드의 결과는 어떨까요?
function b(e) {
console.log(e);
var e;
console.log(e);
e = 7;
console.log(e);
}
b(2);
혹시 답을 1, undefined, 2라고 하신 분이 계시다면, 호이스팅의 개념을 복습하실 필요가 있습니다. 결과는 다음과 같습니다.
-
일단, b함수를 호출해서 b함수영역의 실행컨택스트가 생성되면 환경레코드는 호이스팅된 식별자 e를 수집합니다.
-
매개변수 e를 수집한 이후 또 같은 식별자 e가 들어왔기에 중복된 식별자는 메모리에 추가하지 않습니다.
-
환경레코드를 수집한 뒤 코드를 실행하고 매개변수 e에 2를 할당합니다.
-
첫 번째 console.log(e)엔 2가 출력됩니다.
-
var e, 매개변수 e 모두 같은 식별자를 공유하고 같은 데이터가 들어있습니다. 그래서 var e라는 코드가 기존 e에 할당된 값을 undefined로 바꾸지 않습니다.
-
두 번째 console.log(e)에도 역시 2가 출력됩니다.
-
e에 7을 할당했기에 세 번째 console.log(e)은 7이 출력됩니다.
이전 데이터 타입에서 변수영역에 식별자를 등록하고 값에는 데이터 영역 주소를 넣어준다는 점을 기억하면 당연한 결과입니다. 마찬가지로 다음 예시의 경우 같은 식별자 b를 공유하는 데 호이스팅 시 함수는 할당부까지 값을 가져오기 때문에 첫 번째 console.log(b)엔 함수값이 찍힐겁니다. 그런데 마지막 console.log(b)은 어떨까요?
console.log(b);
var b = "hello";
console.log(b);
function b() {}
console.log(b);
함수선언문의 경우 할당부까지 호이스팅이 되서 해당 컨택스트 내부라면 별도의 초기화 없이 선언문 이전에 함수를 사용할 수 있었죠. 즉, 선언문은 초기화를 하는 부분이 아닙니다. 그래서 function b(){}라는 코드를 지났어도 변수영역 b의 값은 여전히 ‘hello’입니다. 당연히 마지막 console.log(b)엔 ‘hello’가 찍힙니다.
함수선언문 vs 함수표현식
그렇지만 함수를 변수에 담아 초기화를 할 수 있는 경우엔 함수값이 찍힙니다. 대신 할당부가 호이스팅되지 않기 때문에 첫번째 console엔 undefined가 뜹니다.
console.log(b); //undefined
var b = "hello";
console.log(b);
b = () => {};
console.log(b); //함수값
위처럼 익명 함수표현식을 쓰는 게 일반적이지만 기명 함수표현식도 사용은 가능합니다.
b = function c() {};
b();
c(); //에러
하지만 기명함수의 식별자로 호출을 하면 에러가 납니다. b 내부라면 c를 호출해도 에러가 나진 않지만, b를 사용해도 재귀함수를 구현할 수 있는데 굳이 c를 사용할 필요가 없습니다.
함수표현식을 써야 되는 이유
오버라이팅 방지!
함수선언문은 해당 영역에 한해서 전역으로 사용되기 때문에 오버라이팅의 위험이 있습니다. 코드가 길어지고 협업을 하는 경우 이미 다른 곳에 존재하는 식별자를 못 보고 같은 이름으로 함수선언문을 만들 가능성이 있기 때문에 이를 방지하기 위해선 함수 표현식을 사용해야 합니다.
3-2. 스코프
식별자에 대한 유효범위
스코프란 식별자에 대한 유효범위입니다.스코프를 생성하는 방법은 여러가지가 있는데 ES5 이전까지는 함수로만 가능했습니다. 이후엔 var를 제외한 변수들의 경우 블록 단위로 스코프를 생성할 수도 있게 됐습니다. A 스코프 내에서 선언한 변수는 A 스코프와 해당 스코프의 내부함수들 안에서 사용할 수 있습니다. 하지만 A스코프 밖에서 A 스코프의 변수를 사용할 수는 없습니다. 이게 가능한 이유는 Outer Evironment Reference라는 영역 덕분입니다.
3-3. Outer Evironment Reference
컨택스트가 선언됐을 때의 LexicalEvironment
새로운 컨택스트가 B가 실행되면 기존 컨택스트 A의 LexicalEvironment는 B의 Outer Evironment Reference에 담깁니다. 그리고 또 새로운 컨택스트 C가 실행되면 기존 컨택스트 B의 LexicalEvironment가 C의 Outer Environment Reference에 담깁니다. B의 LexicalEvironment엔 A의 LexicalEvironment도 담겨 있으므로 C는 연결리스트 형태로 외부 스코프의 변수를 참조할 수 있게 됩니다. 단 이 때 LexicalEvironment는 해당 컨택스트가 실행된 시점이 아니라 선언된 시점을 의미합니다.
var a = 1;
var A = function () {
var B = function () {
var C = function () {
console.log(a); //4
};
C();
console.log(a); //undefined
var a = 4;
};
B();
console.log(a); //1
};
A();
console.log(a); //1
그래서 위 예시를 보시면 A함수 호출 컨택스트가 실행되고 Outer Evironment Refenence가 참조하는 LexicalEvironment는 A가 선언된 시점의 LexicalEvironment === 전역 컨텍스트의 LexicalEvironment고, B함수 호출시 Outer Evironment Refenence가 참조하는 LexicalEvironment는 B가 선언된 시점의 LexicalEvironment === A의 LexicalEvironment(전역 + A의 환경정보)입니다.
당연히, B 함수 호출 시 C의 정보는 참조할 수 없습니다. 반대로 B 외부의 정보는 Outer Evironment Reference에 있으니 참조할 수 있겠죠.
3-4. 스코프 체이닝
Outer Evironment Reference의 환경레코드를 돌면서 값 찾기
만약 a라는 식별자의 값을 console.log로 출력한다고 가정해보겠습니다. 해당 컨택스트의 환경정보에 a가 있다면, console창엔 코드가 실행된 순간 a에 할당된 값이 출력될 것입니다. 하지만 없다면 Outer Evironment Refence의 환경정보를 가까운 스코프에서 먼 스코프 순으로 뒤져서 해당 식별자에 할당된 값을 출력합니다. 이를 스코프 체이닝이라고 합니다.
var a = 1;
var A = function () {
var B = function () {
var C = function () {
console.log(a); //4
};
C();
console.log(a); //undefined
var a = 4;
};
B();
console.log(a); //1
};
A();
console.log(a); //1
-
위 코드에서 C 함수가 실행됐을 때 C함수의 스코프엔 a정보가 존재하지 않습니다. 그래서 스코프체이닝을 통해 바로 상위 B함수영역의 a의 값 4를 출력하게 됩니다.
-
그런데, B함수의 console.log(a)는 undefined가 뜹니다. 이는 해당 스코프에 a라는 식별자가 있어서 선언은 됐지만 할당부가 호이스팅 되지 않은 상태에서 콘솔창에 출력을 했기 때문입니다.
-
1번과 마찬가지로 C함수를 실행할 때 해당 스코프에 a가 존재하지 않기 때문에 스코프 체이닝을 하는데 바로 상위 전역 컨택스트에 a는 1이 할당되서 console.log(a)는 1이 출력됩니다.
-
내부 스코프에서 a값이 달라졌어도, 전역 스코프는 전역 변수 a의 값을 참조하기에 console.log(a)는 1이 출력됩니다.
3-5. 전역변수와 지역변수
스코프에 대한 이해를 하셨으니 전역변수와 지역변수도 이름만 보고 어떤 변수인지 감이 잡히실 겁니다.
- 전역변수 : 전역 스코프에서 선언한 변수들
- 지역변수 : 함수에 의해 생성된 실행 컨텍스트의 스코프 안 변수들
아까 처음 오버라이팅의 문제가 있다고 한 함수선언문도 전역변수입니다. 전역변수는 실행코드 전반에 영향을 끼치기 때문에 가급적 사용하지 않는 게 좋습니다. 대신 내부함수로 스코프를 분리해서 필요할 때만 해당 변수를 사용하고 수정할 수 있도록 은닉해야 합니다.
4. this
전역 또는 지정된 스코프를 가리키는 객체
실행 컨텍스트의 thisBinding엔 this란 객체가 저장됩니다. 컨택스트가 활성화될 전역 객체가 기본값이 되는 데 지정값이 있다면 해당 객체가 this가 됩니다.
4. 질문
- 다음 코드의 console.log 출력결과를 차례대로 써주세요
var a = 2;
var A = function () {
var B = function () {
var a = 10;
var C = function () {
console.log(a);
};
var a = 4;
C();
console.log(a);
};
B();
console.log(a);
var a = 1;
};
A();
console.log(a);
5. 참고
📖 코어자바스크립트 - 정재남저
댓글남기기