요즘 “코어 자바스크립트”라는 책을 읽고 있다.
책을 읽고 이해한 내용을 좀 더 나의 것으로 만들기 위해 오랜만에 블로그 글을 작성해본다.
(사실 그간 블로그 포스팅을 올리는데 주저되었던 이유 중 하나는 혹시라도 정확하지 않은 내용이 포함되어있거나 온전히 내가 이해하지 못하고 작성한 글은 결국 인터넷을 떠도는 💩을 만드는게 아닌가..라는 생각이 들어 망설였다는.. 게으른 자의 핑계를 대본다.
그래도 결론은 써보자! )
본격적으로 실행컨텍스트에 대한 이야기를 해보자.
실행컨텍스트를 이해하게 되면 자바스크립트의 “호이스팅”과 “스코프 체인”이란 개념 또한 이해할 수 있게된다.
“코드 실행을 위해 필요한 환경정보를 담은 객체”라고 말 할 수 있다.
(처음부터 이렇게 말하면 그 누구가 아핳!하고 명확하게 이해할 수 있겠는가. 적어도 나는 그런 사람이 못된다.)
그래서 실행컨텍스트를 이해하기 위한 약간의 배경지식이 더 필요한데, 그것이 바로 “스택(stack) 과 큐(Queue)”이다.
스택과 큐는 개발자라면 적어도 한번쯤은 들어 봤을 개념이다.
(프론트엔드 개발자의 입장에서) 자바스크립트 실행환경은 브라우저가 일반적일 것이다.
브라우저는 어떻게 자바스크립트 코드를 이해하고 실행시키는 것일까?
간단히만 말하자면, 브라우저의 자바스크립트 엔진엔 ‘스택’이란 데이터 구조가 있어서 여기에 데이터를 하나씩 담고 하나씩 꺼내면서 코드를 실행시킨다.
그리고 이 스택이 비워지면 ‘큐’에서 데이터를 가져와 ‘스택’에 다시 담고 스택은 또 이 데이터를 하나씩 꺼내면서(비워내면서) 코드를 실행한다.
(브라우저 엔진에 대해 이해할 수 있는 글 url)
코드를 실행할 때 스택에 ‘데이터’가 담긴다고 언급했는데
스택에 쌓이는 이 데이터가 바로 “실행컨택스트”이다.
그래서 글 초반에 실행컨텍스트란, “코드실행을 위해 필요한 환경정보를 담은 객체”라고 정의한 연유이다.
그렇다면 실행컨텍스트는 어떻게 이루어져있기에 코드 실행을 위한 환경정보를 제공할 수 있는 것일까?
실행컨텍스트는 크게 아래의 정보를 지닌다.
여기까지만 보면 이게 무슨말일까 싶으니 좀 더 알아보기로 한다.
( 이 컨텍스트 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수는 없다고 한다.)
: 현재 컨텍스트의 식별자들에 대한 정보 + 외부 환경 정보, 선언시점의 Lexical Environment의 스냅샷 ( 변경사항은 반영되지 않음)
여기에 담기는 내용은 Lexical Environment와 같지만 최초 실행시의 스냅샷을 유지한다는 점이 다르다.
실행컨텍스트를 실행할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해 LexicalEnvironment를 만들고 이후엔 주로 LexicalEnvironment를 활용하게 되므로 VariableEnvironment에 대한 설명은 여기까지만.. 하고 빠르게 LexicalEnvironment를 더 알아보자.
: 처음엔 Variable Environment와 같지만 변경사항이 실시간으로 반영
(Environment 너무 기니까 Env라고 써야징)
Lexical Env의 envRecord는 좀 과격하게 말하자면 “식별자 에 미친놈”이다.
envRecord는 코드가 실행되기 전 컨텍스트를 쭉 훑으며 식별자 정보를 모조리 수집한다.
함수에 지정된 매개변수 식별자
var로 선언한 변수의 식별자
함수선언문 자체
(함수 선언문과 함수 표현식의 차이 알려주는 url)
여기서 바로 “호이스팅”이란 개념이 등장한다!
Lexical Env가 식별자 정보를 수집하는 과정이 마치 무언가를 끌어올리는 듯 하다하여
hoisting이란 개념이 생겨난 것이다.
(실제로 코드자체를 끌어올리는 것은 아니지만 개념상 그런 것이므로 “끌어올렸다”고 간주하자)
💡 실무에서의 코드 작성과 연관시켜 생각해보자.
이러한 호이스팅이란 특징때문에 함수선언문으로 작성한 코드는
함수선언이 있기도 전에 실행하는 코드라인이 있어도 에러를 발생시키지 않는다.
(일반적으로 다른 언어에서는 함수를 선언하기도 전에 실행시켜려하면 에러가 발생한다.)⇒ 나는 이런 자유로운 자바스크립트의 특징 (어디에 선언해두어도 실행할 때 에러가 발생하지 않는) 덕에 함수선언문을 즐겨쓰곤했는데 오히려 부작용을 초래할 수 있다는 사실을 이 책을 보며 깨달았다.
⇒ 함수선언문을 사용했을 때 발생할 수 있는 문제상황을 얘기해보자면,
개발자 A가 sum이란 함수선언을 했다. 그런데 이후에 (호이스팅개념을 망각한) 개발자 B도 sum함수를 또 선언하고 테스트 없이 배포까지 해버린다..
이렇게 되면 마지막에 B가 선언한 sum으로 실행되어버리고 어플리케이션은 빠그러지게된다.⇒ 물론 이런 상황이 흔치는 않을 것이라 생각한다. 그럼에도 프로젝트를 진행할 때 혼자만 코드를 작성하는 것이 아니니 “선언한 후에 호출하자 (함수표현식을 쓰자)” 라는 합의를 갖고 코드를 작성하는 편이 더 좋겠다.
//(함수선언문 보다 앞서서) 함수 선언문 실행
a(1)
//함수선언문
function a (val) {
return `${val}입니다.`
}
이번엔 LexicalEnv를 이루는 두번째 요소 outerEnvReference에 대해 알아보자.
다시한번 좀 과격한(?)표현으로 설명하자면 outerEnvironmentReference는 “식별자의 유효범위에 미친놈”이다.
‘식별자의 유효범위’를 안에서부터 바깥으로 차례로 검색해나가는 것을 “스코프체인(scope chain)”이라하고, 이를 가능케하는게 바로 LexicalEnv의 outerEnvReference이다.
outerEnvReference는 현재 호출된 함수가 “선언될 당시의” LexicalEnv를 참조한다.
만약, 함수 A내부에 함수 B를 선언하고 다시 B함수 내부에 함수 C를 선언했고, 현재 함수 C가 호출되어 C의 실행컨텍스트가 활성화되었을 때 C의 outerEnvReference는 (C가 선언될 당시의 LexicalEnv인 ) B의 LexicalEnv를 참조한다. 함수 B의 LexicalEnv의 outerEnvReference는 다시 B가 선언되던 때의 A의 LexicalEnv를 참조한다.
즉, outerEnvReference는 선언 당시 가장 가까운 LexicalEnv를 참조하는 linked list (연결된 리스트)형태로 되어있다.
이런 구조적 특징때문에 여러 스코프에서 동일 식별자를 선언한 경우에 “가장 먼저 발견된 식별자에만 접근 가능하게 된다.”
( 만약 전역 컨텍스트의 LexicalEnv까지 탐색했음에도 해당 변수를 찾지 못하면 undefined를 반환한다.)
전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 칭하고,
그 밖의 함수에 의해 생성된 실행컨텍스트의 변수들을 지역변수라 칭한다.
마지막으로 실행컨텍스트를 이루는 thisBinding에 대해 이야기해보자.
thisBinding에는 this로 지정된 객체가 저장되는데, 함수를 어떻게 호출했는지에 따라 지정되는 객체가 달라진다.
함수를 호출하면서 this를 지정했다면 해당 객체가 this로 지정되지만 달리 지정하지 않으면 전역객체가 this가 된다.
간단히만 말하자면 함수호출 시 앞에 점(.)이 있으면 점 앞에 있는 객체가 this가 되고,
그렇지 않으면 전역객체가 this가 된다.
( this에 대한 내용은 다음에 더 자세히..)