자바스크립트 실행컨텍스트

자바스크립트 실행컨텍스트

코어 자바스크립트( 정재남 著 ) 2장 실행 컨텍스트를 읽고 정리했습니다.

1. 실행 컨텍스트 (Execution context)

1 - 1. 실행 컨텍스트

실행 컨텍스트 : 코드를 실행할 때 제공되는 환경 정보를 모아 둔 객체.

하나의 실행 컨텍스트를 생성하는 방법은 주로 함수를 호출하는 것입니다.

(이외에도 전역공간이나 eval() 함수 등이 있지만 지금은 패스하겠습니다.)

실행 컨텍스트를 직역하면 실행 문맥인데 코드를 실행하는 문맥, 즉 코드를 실행할 때 필요한 환경 정보를 뜻합니다.

그렇다면 코드를 실행할 때 필요한 환경 정보는 무엇이며, 어떻게 생성되는 것일까요?

바로 자바스크립트 엔진이 호출되는 함수와 관련된 정보(=환경정보)를 수집하여 실행 컨텍스트 객체에 전달/저장할 때 생성됩니다.

이 객체(실행 컨텍스트) 는 자바스크립트 엔진이 활용할 목적으로 생성되기 때문에 개발자가 직접 확인할 순 없습니다.

환경정보는 코드를 실행할 때 필요한 정보가 담긴 객체라 하였고, 아래와 같은 정보를 포함합니다.

Variable Environment : 컨텍스트 내 식별자의 정보 + 외부 환경 정보, 선언 시점의 Lexical Environment 의 값 (변경사항 추적 x)

Lexical Environment : 초기화 과정에서는 Variable Environment 와 완전히 동일하나, 이후엔 변경사항이 실시간으로 변경된다.

ThisBinding : 컨텍스트 내의 식별자가 바라봐야 할 대상 객체

1 - 2. 콜스택

// (a) var a = 100; function outer() { function inner() { console.log(a) // undefined var a = 300; } inner(); // (b) console.log(a); // 100 } outer(); // (c) console.log(a); // 1

(a)

자바스크립트 파일이 실행되면(a), 전역 컨텍스트가 콜 스택에 담깁니다.

실행 컨텍스트는 어떤 함수가 호출되면 열리지만, 전역 컨텍스트는 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행됩니다. 즉 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화 된다고 생각해도 됩니다.

(c)

그 후론 (c) 에서 outer() 함수가 호출되면서 실행컨텍스트가 열리며, outer() 함수를 콜스택에 넣습니다.

콜스택의 상단엔 outer 의 실행 컨텍스트가 놓이게 되어 전역 컨텍스트는 다시 상단에 오게 되기 전까지 잠시 중단됩니다.

(b)

outer 의 실행컨텍스트가 열리면서, outer 함수 내부의 코드가 순차적으로 실행됩니다.

그러다 (b) 지점에서 inner() 함수가 호출되면서 inner() 의 실행컨텍스트가 열리고, 콜스택의 최상단은 inner() 가 차지하게 됩니다.

inner() 내부의 코드가 순차적으로 실행되고 끝난 후 콜스택에서 inner() 함수는 pop 됩니다.

다시 outer() 함수가 실행 컨텍스트를 차지합니다. 마찬가지로 outer() 역시 모든 코드를 실행하면 콜스택에서 빠져나오게 되며, 전역 컨텍스트만 남은 콜스택은 마저 전역 컨텍스트를 실행시킵니다.

출처: 코어 자바스크립트

2. Lexical Environment

Lexical Environment : 자바스크립트 코드에서 변수나 함수 등의 식별자를 정의하는 객체

Lexical Environment 는 environment Record 와 outer Environment Reference 로 이루어져 있습니다.

우리가 어떤 내용을 읽고있는지, 잠시 멀리서 바라보도록 하겠습니다

2 - 1. environmentRecord 와 호이스팅

environment Record 는 현재 컨텍스트 내부의 식별자 정보를 저장합니다.

마치 스크립트를 읽듯, 컨텍스트 내부 전체를 처음부터 훑어나가면서 순서대로 식별자 정보를 수집합니다.

컨텍스트를 구성하는 함수의 매개변수 식별자, 컨텍스트 내부의 함수, var, let, const 등으로 선언된 변수 식별자가 environment Record 의 수집대상에 포함됩니다.

컨텍스트 내부의 식별자를 수집한 것과 실행되는 것은 별개의 작업입니다.

즉 코드가 실행되기도 전에, 자바스크립트 엔진은 이미 해당 컨텍스트의 변수(식별자)를 모두 파악하게 되는데 이를 호이스팅이라 부릅니다.

2 - 1 - 0. Hoisting

호이스팅은 일종의 가상 개념인데, 컨텍스트 내부의 변수를 끌어올린다는 의미로 흔히 알려져 있습니다. 자바스크립트 엔진이 식별자를 미리 파악한 것을 일종의 '끌어올림' 으로 형상화하여 표현한 개념입니다. (자바스크립트 엔진이 실제로 식별자를 끌어올리는 것이 아닌, 편의상 그렇게 생각하자고 붙인 가상 개념으로 생각하면 됩니다.)

2 - 1 - 1. 함수 선언문과 함수 표현식

environment Record 는 컨텍스트 내부의 식별자 정보를 수집한다고 했습니다.

이를 유념하며 함수선언문과 함수 표현식을 알아보겠습니다.

함수 선언문 : function 정의부만 존재하고, 별도의 할당 명령이 없습니다. 반드시 함수명이 존재해야 합니다.

함수 표현식 : function 을 별도의 식별자에 할당하는 것을 의미합니다. 함수명이 반드시 정의될 필요 없이, 할당된 식별자를 곧 함수명으로 부릅니다.

// 함수 선언문, a 가 곧 함수명. function a () { ... } // 함수 표현식, 변수명 b 가 곧 함수명. var b = function() { ... }

호이스팅이라는 가상의 개념을 통해 식별자가 끌어올려지는 작업이 일어난다고 생각해보겠습니다.

아래 코드에서 (a) 를 통해 함수 선언문은 전체가 호이스팅 되고,

함수 표현식은 변수 선언부(b)만 호이스팅 된 것을 확인할 수 있습니다.

// before hoisting console.log(sum(1, 2)); console.log(multiply(3, 4)); function sum(a, b) { // 함수 선언문 return a + b; } var multiply = function (a, b) { // multiply: 함수 표현식 return a * b; }; //////// after hoisting //////// var sum = function sum(a, b) { // (a) return a + b; } var multiply; // (b) console.log(sum(1, 2)); console.log(multiply(3, 4)); multiply = function (a, b) { return a * b; };

호이스팅이라는 가상의 과정을 통해 각각 sum 과 multiply 가 console.log() 보다 위로 끌어올려졌습니다. 이 때 sum() 은 함수 선언문이기 때문에 하나의 값으로써 끌어 올려진것을 확인할 수 있습니다.

sum 변수가 메모리 공간의 한 곳을 차지하며, sum 함수 역시 메모리 공간의 다른 곳에 저장됩니다.

sum 변수가 저장된 메모리 공간은 sum 함수의 주소를 참조하며, 해당 함수가 해당 변수에 할당 된 것을 알 수 있습니다.

sum 함수는 sum 변수에 할당돼기 때문에 sum 함수의 주솟값을 sum 변수의 공간에 할당하여 바라보게 합니다.

반면에 함수 표현식인 multiply 는 변수 multiply 만 호이스팅 될 뿐, 해당 메모리 공간이 참조할 주소는 비어있습니다. 이 상태로 console.log(multiply(3, 4)) 를 찍으면 'multiply is not a function' 이란 에러메시지만 출력 될 뿐입니다.

(b) 에서 볼 수 있듯 multiply 는 그저 식별자에 불과하기 때문입니다.

다시 environmentRecord 의 관점에서 살펴보면, 해당 컨텍스트의 environmentRecord 는 sum 식별자와 함수정의부를, 그리고 식별자 뿐인 multiply 를 저장했다고 볼 수 있습니다.

지금까지 맥락을 살펴보면 에러를 내지 않는 함수 선언문이 짱짱 아닌가? 하고 생각하실수도 있습니다.

하지만 현업에서는 대부분 함수 표현식으로 코드를 작성하는 것을 볼 수 있는데, 그 이유를 알아보겠습니다.

2 - 1 - 2 함수 표현식이 더 안전한 이유

console.log(sum(1, 2)); // (a) function sum(a, b) { return a + b; } var a = sum(1, 2); // (b) /// after 1000 line /// function sum(a, b) { return a + '+' + b + '=' + (x + y); } var c = sum(1, 2); // (c) console.log(c) // (d)

위 코드에서 sum() 함수는 함수 선언문의 형태로 작성되었습니다. 그렇기 때문에 (a) 에서 무리 없이 sum(1,2) 의 결과값인 3을 출력할 것이라 예측할 수 있습니다.

하지만 1000 줄 이후, 코드가 너무 길어져 기존에 sum() 함수의 존재를 잊고 실수로 동일한 이름의 sum() 함수가 재정의 한다고 가정해봅시다. 문자열을 리턴하는 두번째 sum() 함수 역시, 호이스팅 되어 위로 끌어올려지는데, 이 때 기존에 숫자를 리턴하던 함수의 내용을 덮어버리는 상황(오버라이드)이 발생합니다.

1000 줄 이전의 코드에서는 당연히 (a) 에서 숫자형 값이 출력될 것이라 믿었지만, 오버라이드 된 sum() 함수로 인하여 (a) 에서도 뜬금없는 문자열 값이 출력되는 상황이 벌어지게 됩니다. 설상가상으로 에러 검출도 되지 않기 때문에 어느 부분이 문제인지 더 찾기 힘들어지겠죠?

극단적이지만 함수 선언문이 가지는 위험성을 보여주는 예시였습니다.

만약 위와 같은 상황에서 함수 표현식이라면 어떻게 됐을까요?

함수 표현식의 경우, 함수가 할당된 변수만 호이스팅 되기 때문에 다른 함수를 오버라이드할 가능성이 없습니다.

console.log(sum(1, 2)); // (a) -- Uncaught Type Error: sum is not a function var sum = function (a, b) { return a + b; } var a = sum(1, 2); // (b) /// after 1000 line /// var sum = function(a, b) { return a + '+' + b + '=' + (x + y); } var c = sum(1, 2); // (c) console.log(c) // (d)

(a) 부분에서 곧바로 에러가 출력되는 것을 볼 수 있는데, 함수표현식의 경우 sum 변수 선언부만 호이스팅되어 undefined 상태이기 때문입니다.

비록 (a) 에서는 에러가 발생할 수 있지만, 해당 부분만 지나면 함수 선언문에서 마주했던 문제를 피할 수 있습니다.

1000줄 이후에서 동일한 이름으로 sum 함수를 선언해도, (b) 와 (c) 에서 개발자가 의도한대로 값을 출력할 수 있습니다.

만약 상수로 함수표현식을 나타낸다면, 동일한 이름의 함수에 대해 Syntax Error 를 발생하여, 사전에 에러를 방지할 수도 있습니다.

그 아래의 sum() 함수선언문의 경우 그대로 오버라이드(두번째 sum 이 첫번째 sum 을 덮어버린다) 되는것을 이제는 이해할 수 있습니다.

정리

1. 전역공간에 함수를 선언하거나, 동명의 함수를 중복 선언하는 경우는 자제해야합니다.

2. 전역공간에 동명의 함수가 존재하는 상황이더라도, 해당 함수들이 함수 표현식으로 정의됐다면 위 같은 에러 상황은 피할 수 있습니다.

3. const 상수에 함수 표현식을 할당한다면, (협업과정에서) 동명의 함수를 만드는 문제를 방지할 수 있습니다.

2 - 2. 스코프, 스코프 체인

스코프 scope : 식별자의 유효범위

자바스크립트는 함수에 의해 스코프가 주로 발생하는데, ES6 부터는 블록에 의해서도 스코프 경계를 발생시킬 수 있습니다. 스코프 경계는 어떤 상황에서 사용될까요?

스코프란 식별자의 유효범위라 했습니다. 유효범위는 접근할 수 있는 영역 정도로 하겠습니다.

const a = "a" const bScope = () => { const b = "b" ... }

상수 a로 선언한 식별자의 스코프는 전역공간 뿐만 아니라, 화살표함수 bScope 내부까지 접근할 수 있습니다.

즉 a 식별자의 유효범위는 전역공간 + bScope() 내부 라고 볼 수 있습니다.

하지만 bScope() 내부에서 선언된 b 식별자는, 해당 함수 외부에서 접근할 수 없습니다. b 식별자의 유효범위는 딱 bScope() 내부입니다.

2 - 2 - 1. 스코프, 스코프 체인, outerEnvironmentReference

Lexical Environment 는 outerEnvironmentReference 와 environmentRecord 로 이루어져 있습니다.

그 중 outerEnvironmentReference 는 호출된 함수가 선언될 당시의 LexicalEnvironment 를 참조합니다.

이를 조금 풀어 설명하면, a 함수 내부에 b 함수가 선언돼 있다면, b 함수의 outerEnvironmentReference 는 a 함수의 LexicalEnvironment 를 참조합니다. 왜냐하면 b 함수가 선언될 당시, a 함수의 스코프에 있었기 때문입니다.

글로는 잘 와닿지 않습니다. 코드를 살펴보겠습니다.

var a = 1; var outer = () => { var middle = () => { console.log('(1)', a); // (1) undefined var a = 3; var inner = () => { console.log('(2)', a); // (2) undefined var a = 5; } inner(); } middle(); console.log('(3)', a); // (3) 1 } outer(); console.log('(4)', a); // (4) 1

middle 함수는 outer 함수의 내부에서, inner 함수는 middle 함수의 내부에서 선언되어 있습니다.

outerEnvironmentReference 를 대입해서 말하면,

middle 함수의 outerEnvironmentReference 는 outer 함수의 LexicalEnvironment 를 ,

inner 함수의 outerEnvironmentReference 는 middle 함수의 LexicalEnvironment 를 참조합니다.

마치 연결리스트(linked list) 같은 형태를 띄는 것을 볼 수 있습니다.

그렇다면 outerEnvironmentReference 가 참조하는 LexicalEnvironment 가 어쨌다는 걸까요?

우선 middle 함수 내부와 그 내부에서 선언된 inner함수를 먼저 살펴보겠습니다.

middle 함수의 LexicalEnvironment 는 우선 식별자 a 와 inner 함수를 EnvironmentRecord 에 담습니다.

inner 함수의 LexicalEnvironment 는 inner() 내부의 식별자 a 를 EnvironmentRecord 에 담고, middle 함수의 LexicalEnvironment 를 참조하여 outerEnvironmentReference 에 담습니다.

inner() 함수의 내부에는 a 식별자를 콘솔로그에 출력하라고 하지만, (호이스팅 된) a 식별자엔 어떠한 값도 (아직) 할당되지 않은 상태이기 때문에 undefined 를 출력하게 됩니다.

inner() 함수 내부에 식별자 a 가 있다면, 외부에서 선언한 동일한 이름의 a 변수에는 접근할 수 없고, 이를 변수 은닉화 라고 합니다.

여기서 만약 inner 함수 내부에 var a = 5 라는 코드가 없었다면 어떻게 됐을까요?

inner 함수 내부에 a 식별자가 없으니 a 를 출력하는 콘솔로그는 여전히 undefined를 출력할까요?

여기서 스코프체인과 outerEnvironmentReferece 가 효과를 발휘합니다.

// inner 함수 내부에 a가 선언되지 않은 경우 var a = 1; var outer = () => { var middle = () => { console.log('(1)', a); // (1) undefined var a = 3; var inner = () => { console.log('(2)', a); // (2) 3 } inner(); } middle(); console.log('(3)', a); // (3) 1 } outer(); console.log('(4)', a); // (4) 1

(2) 부분에서 a 의 값은 undefined 가 아닌 3이 출력됩니다.

눈치 채셨겠지만, inner함수의 outerEnvironmentReference가 middle 함수의 LexicalEnvironment 를 참조하기 때문에, middle의 LexicalEnvironment 에 저장된 a의 값인 3을 출력하게 되는 것입니다.

자신의 범위 바깥의 식별자 a 에 접근했다고 생각할 수도 있지만, 외부 스코프에서 내부로 접근한 것으로 보는 것이 올바른 관점입니다. inner 내부에 있는 식별자를 바깥에서는 참조할 수 없지만, 역으로 inner 외부의 식별자를 inner 내부로 가져와 사용(접근)하는 것은 가능하기 때문입니다.

정리 및 요약

1. 실행 컨텍스트는 실행되는 코드에 제공되는 환경 정보들을 모아놓은 객체입니다.

2. 실행 컨텍스트의 LexicalEnvironment 는 environmentRecord 와 outerEnvironmentReference 로 구성돼 있습니다.

3. environmentRecord 는 매개변수명, 변수 식별자, 함수명 등을 수집하며, outerEnvironmentReference 는 직전 컨텍스트의 LexicalEnvironment 정보를 참조합니다.

4. 변수 선언과 값 할당이 동시에 이루어진 문장은 선언부만을 호이스팅하고, 할당 과정은 원래 자리에 남아있습니다. 이 때문에 함수표현식과 함수선언문의 호이스팅 차이가 발생합니다.

5. 어떤 변수에 접근할 때, 현재 컨텍스트의 LexicalEnvironment 를 탐색하여 해당 값을 발견하면 그 값을 반환, 그러지 못할 땐 outerEnvironmentReference 에 담긴 LexicalEnvironment 에서 해당 변수를 찾는 과정을 반복합니다.(스코프 체인) 만약 전역 컨텍스트까지 해당 변수를 찾지 못하면 undefined 를 반환합니다.

from http://junior-datalist.tistory.com/204 by ccl(A) rewrite - 2021-09-26 02:27:30