JS - 실행 컨텍스트
실행 컨텍스트란?
execution context, 실행 컨텍스트란 간단하게 말하면 실행할 코드에 제공할 환경 정보를 모아놓은 객체이다. 시작부터 말이 어렵지만 이해한다면 자바스크립트가 어떻게 동작하는지 실행 원리를 이해할 수 있을 것이다.
데이터 구조
실행 컨텍스트를 알아보기 전에 데이터 구조인 스택(stack)과 큐(queue)를 짚고 넘어가겠다. 이걸 알아야 실행 컨텍스트로 인해 어떤 데이터가 동작하는지 이해할 수 있기 때문이다.
stack
스택은 우물과 같은 구조이다. 단어에서도 알수 있듯이 하나씩 쌓이는 데이터 구조이다.
a,b,c,d순으로 데이터가 들어게되면 다시 데이터를 꺼내는 시점에서는 d,c,b,a순서 즉 역순으로 꺼낼수 밖에 없다.
queue
큐는 원통 구조이다. queue라는 단어는 대기줄이라는 뜻으로 들어온 순서대로 나가는 것이다.
식당이나 물류 관련된 일을 해봤다면 선입선출이라는 단어가 익숙할 것이다. 큐의 데이터 구조와 같은 의미이다.
실행 컨텍스트와 스택
실행 컨텍스트와 관련있는 구조는 스택이다. 실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아둔 객체라고 했다. 특정 코드가 실행되면서 실행 컨텍스트를 구성하게 되고, 콜 스택(call stack)에 쌓아 위에 쌓여진 순서대로 실행 컨텍스트와 관련된 코드를 실행하는 것이다.
예제를 통해서 어떻게 컨텍스트가 스택에 쌓이는지 알아보겠다.
var a = 1; // (1) function outer(){ function inner(){ console.log(a); var a = 3; } inner(); // (2) console.log(a); } outer(); // (3) console.log(a);
위와 같은 코드를 실행하는 순간(1) 전역 컨텍스트가 콜 스택에 담긴다.
전역 컨텍스트는 실행 컨텍스트와 다를 것은 없다. 하지만 굳이 차이점을 찾자면 대상이 함수가 아닌 전역 공간이기 때문에 arguments가 없다. 가장 외부에 있는 컨텍스트이기 때문에 전역 스코프 한개만 존재한다.
전역 컨텍스트와 관련된 코드를 실행하다가 outer함수(3)가 실행되면서 outer와 관련된 정보를 수집에 실행 컨텍스트를 생성하고, 콜 스택에 담는다. 다시 outer를 실행하면서 inner함수를 동작하게 되고 inner와 관련된 정보를 수집에 실행 컨텍스트를 생성하고 콜 스택에 담는다.
그림으로 표현하면 이런 형식으로 실행 컨텍스트가 담기고 있는 것이다. 아까 스택을 알아볼때 얘기한것처럼 담긴 순서로 진행되는 것이 아니라 나중에 담긴 것부터 실행하게 된다. 그래서 inner함수까지 스택에 담기면 inner함수부터 실행하게 되고, 함수가 종료되면 스택에서 제거된다. 이후 outer, 전역 컨텍스트도 동일하게 동작한다.
이런 구조를 가지고 코드가 동작하게 되니 실행 컨텍스트가 콜 스택 위에 쌓이는 순간이 현재 실행할 코드에 관여하게 되는 시점이 된다. 그래서 자바스크립트는 해당 함수에 관련된 데이터를 실행 컨텍스트에 객체로 저장하게 된다.
총 3가지의 정보가 담기게 된다. 우선 VariableEnviroment는 현재 컨텍스트 내의 식별자 정보와 외부 환경 정보, 선언 시점의 LexicalEnviroment의 스냅샷으로 변경되지 않는다. LexicalEnviroment는 처음엔 VariableEnviroment와 같지만 변경사항이 실시간으로 적용된다. 마지막으로 ThisBinding은 this식별자가 바라봐야할 대상 객체이다.
이 객체는 사용자가 확인하거나 수정할 수 없고 자바스크립트 엔진이 활용할 목적으로만 생성된다. ThisBinding은 뒤에서 this와 함께 다시 알아볼 것이다.
VariableEnviroment
위에서 간단하게 얘기했지만 실행컨텍스트가 생선되는 시점에 스냅샷을 유지한다. 즉 초기값이 변하지 않고 유지된다는 것이다. 이후에 변하는 값들은 LexcialEnviroment에 저장된다.
LexicalEnviroment
VariableEnviroment와 같은 시점에 생성되고 같은 내용을 가지고 있지만 데이터가 실시간으로 변하는 특성을 가지고 있다. Lexical이라는 단어의 의미를 정확하게 정의하고 정리하면 좋겠지만 한마디로 정의하기 애매한 단어이다. 그러면 우리는 어떻게 받아들이면 될까?
실행 컨텍스트를 구성하는 환경 정보들을 모아둔 사전적인 정보의 집합
실행 컨텍스트가 동작하기 위해 필요한 환경 정보는 변할수 있기 때문에 정적인 정보가 아닌 유동적이다. 그래서 사전 정보라기보단 유기적인 정보를 모아두었다 정도로 이해하면 되지 않을까 싶다. 그래서 이 개념은 느낌만 익히고 단어 자체를 익히는게 낫다.
이제 L.E(렉시컬환경을 줄여서 사용하겠다.)내부에 존재하는 저장소를 알아보겠다.
enviromentRecord
현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트의 매개변수 식별자, 함수, var로 선언된 변수 등이 순서대로 수집된다. 실행 컨텍스트가 생성되면서 수집하기때문에 자바스크립트 엔진은 함수를 실행하기 전에 함수 내부 코드의 변수명들을 알게된다. 이러한 것을 호이스팅이라고 한다.
hoisting
호이스팅은 끌어올린다는 뜻을 가진 것처럼 변수를 끌어올린것으로 가정하는 가상의 개념이다.
매개변수와 변수에 대한 호이스팅
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가 나올것만 같은 느낌이다. 하지만 우리 생각과는 조금 다르다.
우선 argument로 1을 지정한것은 LexicalEnviroment입장에서는 코드 내부에서 선언한 것과 다른 점이 없다. 왜냐하면 실행 컨텍스트가 생성되는 시점에 함께 만들기 때문이다.
function a (x){ var x = 1; console.log(x); // (1) var x; console.log(x); // (2) var x = 2; console.log(x); // (3) } a();
이제 선언부를 호이스팅 시켜보겠다.
function a (x){ var x; var x; var x; x = 1; console.log(x); // (1) console.log(x); // (2) x = 2; console.log(x); // (3) } a();
변수를 선언한 선언부를 상단으로 끌어올리고 변수에 값을 할당하는 코드는 원래 위치에 있게 된다. 그럼 처음 예상했던 결과가 아닌 다른 결과가 예상될 것이다. (1) 1, (2) 1, (3) 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가 나올것만 같다. 하지만 호이스팅을 적용시켜보겠다.
function a (){ var b; function b (){} console.log(b); // (1) b = 'bbb'; console.log(b); // (2) console.log(b); // (3) } a();
함수는 변수와 다르게 선언부만 끌어 올리는 것이 아니라 전체를 끌어올린다. 그래서 var와 같이 상단으로 올라간 모습이다. 추가적으로 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것으로 여길수 있기 때문에
function a (){ var b; var b = function b (){} console.log(b); // (1) b = 'bbb'; console.log(b); // (2) console.log(b); // (3) } a();
이렇게 호이스팅까지 적용한 코드이다. 그럼 이제 올바른 동작을 예상해보면 (1) 함수 b, (2) bbb, (3) bbb라는 결과가 나온다.
호이스팅에서의 함수 선언문과 함수 표현식
호이스팅과 관련해 함수 선언문과 함수 표현식은 차이가 존재한다.
- 함수 선언문 : function의 정의문만 존재하고 할당 명령이 없음, 함수명 필수
- 함수 표현식 : function을 별도의 변수에 할당하는 것, 함수명 필수 아님
function a(){} // 함수 선언문 a(); // OK ... var b = function (){} // (익명) 함수 표현식, 함수명 : b b(); // OK ... var c = function d(){} // (기명) 함수 표현식, 변수명 : c, 함수명 : d c(); // OK d(); // ERROR
이런 차이가 존재하기 때문에 호이스팅 관점에서도 차이가 존재한다.
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; }
앞에서 했던 호이스팅 개념을 적용시키면 함수 선언은 상단으로 호이스팅되고, 변수에 함수를 할당한 부분은 변수 선언만 호이스팅되고 할당은 원래 위치에서 이뤄진다.
var sum = 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은 실행되기 전에 함수가 정의되있기 때문에 3이라는 결과가 나왔고, multiply는 선언만하고 할당은 실행 이후에 되고 있기 때문에 multiply is not a function에러가 발생한다.
let과 const의 호이스팅?
일반적인 상식으로 우리는 var는 호이스팅되지만 let과 const는 호이스팅 되지 않는다고 알고있다. 하지만 이건 잘못된 개념이다. 물론 호이스팅이라는 개념 자체가 자바스크립트의 동작 방식을 편하게 하기 위한 추상적인 개념이고 let과 const는 ES6이후에 추가된 것이기 때문에 간극이 발생한 것이다.
이 차이를 이해하기 위해선 변수가 어떻게 선언되는지 알아야한다. 자바스크립트에서 변수는 3가지 단계를 거치게 된다.
1.Declaration phase(선언 단계, L.E에 등록)
2.Initialization phase(초기화 단계, 메모리 할당 -> 초기값 undefined)
3.Assignment phase(할당 단계, 특정값 할당)
var는 생성되는 시점에 1,2단계가 동시에 진행된다. 그래서 var는 선언한 이후에도 undefiend로 확인이 가능하다. 하지만 let과 const는 1단계만 실행한다. 그래서 이 단계를 TDZ(Temporal dead zone)이라고 한다. 할당하기 전까지는 이 단계에 머물러 있기 때문에 let과 const로 선언한 변수는 접근시에 reference error가 발생하는 것이다.
즉, let과 const는 var와 동일하게 실행 컨텍스트가 생성되는 시점에 호이스팅되서 L.E에 등록된다.
L.E와 V.E의 존재 이유
그럼 왜 V.E와 L.E가 따로 존재하는지 추론이 가능하다. let과 const가 존재하지 않던 ES6이전에는 var만 존재했기 때문에 실행 컨텍스트가 생성되는 시점에 V.E와 L.E의 구분이 필요가 없었다. 하지만 ES6이후에 블록 스코프라는 개념이 생기면서 var와 let, const를 구분할 필요가 있어지면서 나눠진 것이다.
var는 함수 스코프이기 때문에 생성시에 V.E에 등록하고 값을 undefined로 초기화한다. 그리고 이후에 변하는 값들은 L.E에서 관리한다. 반면에 let과 const는 블록 스코프이기 때문에 선언시에 값을 초기화하지 않는다. 그래서 초기화가 필요한 var는 VariableEnviroment에 등록하고 초기화를 진행하고, let과 const는 초기화하지 않기 위해 LexicalEnviroment에 등록하는 것이다. 결론적으로 함수 스코프인 var와 블록 스코프인 let, const를 구분하기 위함이다.
개인적인 궁금함에 찾아보긴 했지만 완벽하게 이해했는지는 애매하다... 나중에 따로 정리를 다시 해보는게 좋겠다.
outerEnviromentReference와 스코프 체인
L.E의 두번째 수집 정보인 outerEnviromentReference에 대해서 알아보겠다. 우선 scope, 스코프란 식별자에 대한 유효 범위다.
var a = 1; function b(){ var c = 2; console.log(a); // 1 console.log(c); // 2 } b(); console.log(a); // 1 console.log(c); // reference error
이 코드에서 a는 함수 밖에서 선언되었기 때문에 함수 내부에서도 호출이 가능하지만 c는 b함수 내부에서 선언되었기 때문에 접근이 안된다. 이와같이 접근 가능한 범위를 스코프라고 한다. 코드를 실행하면서 유효범위를 안에서부터 바깥으로 검색하는 것을 scope chain이라고 하며 이걸 가능하게 하는 것이 outerEnviromentReference이다.
outerEnviromentReference은 호출된 함수가 선언될 당시에 L.E를 참조한다. 예시를 통해서 좀더 쉽게 설명해보겠다.
var a = 1; var outer = function (){ var inner = function (){ console.log(a); // (1) var a = 3; } inner(); console.log(a); // (2) } outer(); console.log(a); // (3)
이 코드에서 컨텍스트가 어떻게 생성되고 스코프가 어떻게 생성되는지 순서를 적어보겠다.
- 코드 실행시 전역 컨텍스트 활성화, 전역 컨텍스트 enviromentRecord에 {a, outer} 저장
- a라는 변수에 1, outer라는 변수에 함수 할당
- outer호출, outer 실행 컨텍스트 생성, enviromentRecord에 { inner } 저장
- outer의 outerEnviromentReference에 outer가 선언될 당시의 앞에 존재하는 컨텍스트의 L.E를 참조 복사
- inner라는 변수에 함수 할당
- inner호출, inner 실행 컨텍스트 생성, enviromentRecord에 { a } 저장
- inner의 outerEnviromentReference에 inner가 선언될 당시의 앞에 존재하는 컨텍스트의 L.E를 참조 복사
- inner에서 a에 접근, inner 컨텍스트의 enviromentRecord에서 a 발견, 값이 없음으로 undefined 출력
- a에 3할당, inner 컨텍스트 종료 및 제거, outer 실행 컨텍스트 재개
- outer에서 a에 접근, outer 컨텍스트의 enviromentRecord에 없음으로 outerEnviromentReference검색
- a에 저장된 1출력
- outer 컨텍스트 종료 및 제거, 전역 컨텍스트 재개
- 전역에서 a에 접근, 전역 컨텍스트 enviromentRecord에서 a발견, a에 저장된 1출력
과정이 엄청 길지만 간단하게 정리해보자면 가장 가까운 요소부터 차례대로 접근하며 동일한 식별자를 선언한 경우에는 스코프 체인 상에서 가장 먼저 발견된 식별자에게만 접근이 가능하다. 그래서 전역->outer->inner로 함수 규모는 작아지지만 반대로 스코프를 통해 접근 가능한 변수의 수는 늘어난다.
추가적으로 inner함수를 보면 전역에서도 a를 선언하고 내부에서도 a를 선언했다. 하지만 스코프 체인의 첫번째 인자인 inner 스코프의 L.E부터 검색을 하게된다. 만약 이곳에 원하는 식별자가 없다면 outerEnviromentReference에서 검색을 하는 것이다. 이렇게 내부 함수에서 동일한 변수를 선언한 경우를 변수 은닉화라고 한다.
전역변수와 지역변수
그렇다면 이 두 개념은 쉽게 추론이 가능하다. 전역 스코프에서 선언한 값이 전역변수가 되고, 함수 내부(스코프)에서 선언한 값이 지역 변수가 되는 것이다. 전역변수를 써야만 하는 상황이라면 모르겠지만 전역변수를 선언한 순간 모든 함수에서 접근이 가능하기 때문에 코드의 안정성을 위해 되도록 지역 변수를 사용하는것을 권장한다.
마무리
실행 컨텍스트까지 정리를 마쳤다. 직접 정리를 해보면서 검색도 해보고 책도 계속 읽어보니 조금은 이해가 되는것 같다. 이런걸 만든 사람은 얼마나 머리가 좋은걸까싶은 마음이 든다. 나도 이런 굵직한 것들을 할수나 있을까 걱정은 되지만 언젠간 이런 쪽으로 머리가 더 잘 돌아가려나 싶은 생각이 있다. 다음은 this를 정리해보겠다. 아마 쉽지 않지 않을까...
개의 댓글
2
실행 컨텍스트란?
데이터 구조
stack
queue
실행 컨텍스트와 스택
VariableEnviroment
LexicalEnviroment
enviromentRecord
hoisting
매개변수와 변수에 대한 호이스팅
함수 선언의 호이스팅
호이스팅에서의 함수 선언문과 함수 표현식
let과 const의 호이스팅?
L.E와 V.E의 존재 이유
outerEnviromentReference와 스코프 체인
전역변수와 지역변수
마무리