본문 바로가기

02. 실행 컨텍스트

본 내용은 책 코어 자바스크립트(2019, 정재남)를 세 번 정독 후, 글쓴이가 이해한 내용과 실제 업무에서 겪은 경험을 기술하였습니다. 다소 형식적이지 못합니다.

 

수정 이력

  • 2021/06/12 그림 및 설명 수정

 


 

1. 실행 컨텍스트

실행 컨텍스트란 실행할 코드에 줄 환경 정보들의 객체를 말한다. 이 컨텍스트들을 콜 스택에 push해두고, top에 위치한 컨텍스트와 관련된 코드를 실행한다. 이러한 컨텍스트를 구성할 수 있는 대표적인 방법은 함수를 호출하는 것이다.

 

var name = 'newjeong'; // --- (0)

function hello (a) { // --- (2)
    function sayhello () { // --- (4)
        console.log('hello, ' + name); // --- (5)
        var name = 'everyone';
    }
    
    sayhello(name); // --- (3)
    
    console.log(name); // --- (6)
}

hello(name); // --- (1)
console.log(name); // --- (7)

예제 1. 

 

예제 1의 주석 순으로 실행 컨텍스트의 구성을 따라가보면 된다.

사실 제~~일 처음 (0) 이전에 자바스크립트 파일이 열리는 순간, 전역 컨텍스트가 콜스택에 쌓이게 된다.

 

(0) 전역 컨텍스트에 식별자 name 정보와 함수 hello 선언이 저장된다. 2에 자세하게 설명 예정.

 

(1) hello 함수가 실행되면 hello 함수의 실행 컨텍스트가 생성되고 콜스택에 저장된다.

 

(2) 이제 hello 실행 컨텍스트가 스택의 top에 위치하므로 전역 컨테스트 코드 실행을 중단하고, hello 실행 컨텍스트와 관련된 코드를 실행한다.

 

(3) sayhello 함수를 호출하고, (1)과 마찬가지로 sayhello 실행 컨텍스트를 콜스택에 저장한다.

 

(4) 이제 sayhello 실행 컨텍스트가 콜스택의 top이므로, hello 실행 컨텍스트를 중단하고, sayhello 함수 내부 코드를 실행한다.

 

(5) sayhello 내부 코드를 실행한다.

 

(6) sayhello 함수가 종료된 후, 콜 스택에서 제거되고 나면 다시 hello 실행 컨텍스트가 콜 스택의 top이므로 hello 내부 코드를 실행한다.

 

(7) hello 함수가 종료된 후, 콜 스택에서 제거되고 다시 전역 컨텍스트 코드를 실행한다.

콜스택의 흐름

 

2. 실행 컨텍스트의 정보

실행 컨텍스트에 저장되는 정보는 크게 VariableEnvironment, LexicalEnvironment, ThisBinding이다.

 

VariableEnvironment

함수가 선언될 시점의 내부 식별자들의 정보, 외부 환경 정보를 저장. 처음 생성된 후, 변하지 않는다.

LexicalEnvironment

VariableEnvironment와 동일한 정보를 저장하지만, 변경 사항이 실시간으로 반영된다.

ThisBinding

this 식별자가 가리키는 객체

실행 컨텍스트를 처음 구성할 때, VariableEnvironment를 생성하고, 이를 그대로 복사해서 LexicalEnvironment를 생성한다. VariableEnvironment는 처음 실행 시의 스냅샷이며 이후 실행하면서 변경되는 정보는 LexicalEnvironment에 저장된다.

따라서 주로 LexicalEnvironment의 변화를 추적하면서 함수의 실행을 살펴보게 된다.

실행 컨텍스트의 구조

 

3. LexicalEnvironment

LexicalEnvironment는 크게 environmentRecordouterEnvironmentReference로 구성된다.

 

environmentRecord

실행 컨텍스트 내부의 매개변수, 식별자, 함수 정보

outerEnvironmentReference

자신이 선언될 당시의 LexicalEnvironment

 

4. EnvironmentRecord, Hoisting

enviromentRecord에는 함수의 매개변수, 내부 식별자, 내부에 선언된 함수의 정보를 저장하고 있다고 위에서 언급했다. 실행 컨텍스트를 구성할 당시에 코드 전체를 훑으면서 순서대로 그 정보를 수집한다.

 

즉, 코드를 실행하기 전에 관련 정보를 최상단으로 먼저 끌어올린다. 이것을 호이스팅(Hoisting)이라고 한다.

hosit의 사전적 의미

 

 

호이스팅의 핵심 규칙은 변수의 식별자만 최상단에 저장하고, 할당 과정은 원래 순서대로 실행된다는 것이다.

function foo (a) { // 매개변수 a
    console.log(a); // --- (1)
    
    var a; // 첫번째 내부 식별자 a
    
    console.log(a); // --- (2)
    
    var a = 2; // 두번째 내부 식별자 a
    
    console.log(a); // --- (3)
}

foo(1);

 

위 예시 코드에서 (1), (2), (3)에 무엇이 출력될까.

코드 순서대로 읽으면서 답해보면 아래처럼 생각하기 쉽다.

 

(1): 1 (foo의 매개변수)

(2): undefined (내부 변수로 새로 선언)

(3): 2 (새로 선언한 내부변수에 2를 할당)

 

그러나 호이스팅의 핵심 규칙을 적용해보면 다음과 같이 코드를 바꿀 수 있다.

(실제로 엔진이 이렇게 바꾸는 건 아니다)

호이스팅의 가장 핵심 규칙은 변수의 식별자만 최상단에 저장하고, 할당 과정은 원래 순서대로 실행된다는 것이다.
function foo () {
    var a; // 매개변수 a
    var a; // 첫번째 내부 식별자 a
    var a; // 두번째 내부 식별자 a
    
    a = 1; // 매개변수 a 할당
    
    console.log(a); // --- (1)
    
    console.log(a); // --- (2)
    
    a = 2; // 두번째 내부 식별자 a 할당
    
    console.log(a); // --- (3)
}

 

 

식별자 a는 호이스팅 규칙에 의해 최상단에 정의된다. 첫 번째와 두 번째 내부 식별자 a의 선언은 이미 매개변수 a가 존재하므로 무시된다.

 

그 후, foo 실행 시 매개변수로 주었던 1이 a에 할당되어 (1), (2)의 결과는 1이 출력될 것이다.

 

두번째 내부 식별자 a에 할당했던 2는 원래 순서대로 실행되어 (3)에서 2가 출력될 것이다.

 

따라서 답은 (1) : 1, (2); 1, (3): 2이다.

 

이번엔 함수를 보자.

function foo () {
    console.log(a); // --- (1)
    
    var a = 'javascript';
    
    console.log(a); // --- (2)
    
    function a () {}
    
    console.log(a); // --- (3)
}

 

 

두 번째 console.log 다음에 a를 function으로 정의했다.
앞에서 호이스팅 규칙을 배웠으니 다음처럼 호이스팅될 것이라고 생각하기 쉽다.

function foo () {
    var a; // 식별자 a는 호이스팅 되었고 ...
    
    console.log(a); // --- (1)
    
    a = 'javascript'; // 여기서 할당했고!
    
    console.log(a); // --- (2)
    
    a = function () {} // 함수도 여기서 할당하고!
    
    console.log(a); // --- (3)
}

따라서 각 콘솔의 결과로

(1): undefined

(2): 'javascript'

(3): function () {}

으로 생각하기 쉽지만 여기서 호이스팅의 함수와 관련된 규칙이 추가된다.

변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면 함수 선언은 함수 전체를 끌어올립니다.

따라서 다음과 같이 호이스팅된다.

function foo () {
    var a; // 식별자 a 호이스팅
    
    var a = function () {}; // 함수 선언문 자체를 호이스팅
    
    console.log(a); // --- (1)
    
    a = 'javascript';
    
    console.log(a); // --- (2)
    
    console.log(a); // --- (3)
}

 

 

따라서 정답은

(1): function () {}

(2) 'javascript'

(3): 'javascript'

이다.

결론적으로 호이스팅의 핵심 규칙은 다음 두가지를 기억하고 있으면 된다.

규칙 1. 변수의 식별자만 최상단에 저장하고, 할당 과정은 원래 순서대로 실행
규칙 2. 함수 선언은 함수 전체를 끌어올림

 

함수 선언문, 함수 표현식

호이스팅과 함께 알아두면 좋은 내용으로 책에 소개되었다.

함수의 선언을 표현하는 방법으로 함수 선언문과 함수 표현식 두 가지 방식이 있다.

function a () {} // 함수 선언문

a();

var b = function () {} // 함수 표현식 (익명)

b();

var c = function d () { // 함수 표현식 (기명)
    c();
    d();
} 

c();

d(); // Error

 

호이스팅 관점에서 함수 선언문과 함수 표현식은 차이가 있다.

함수 선언문은 선언문 자체가 호이스팅되는 반면에 함수 표현식은 마치 변수처럼 식별자인 함수명만 호이스팅되고, 실제 함수 내용은 원래 할당된 위치에서 정해진다.

console.log(sayHello('newjeong')); // 'hello, newjeong'
console.log(sayBye('newjeong')); // Error! sayBye is not a function.

function sayHello (name) {
    return 'hello, ' + name;
}

var sayBye = function (name) {
    return 'goodbye, ' + name;
}

 

 

위 예시 코드에서 확인가능하듯 sayHello 선언문은 호이스팅되지만, sayBye는 console문 다음에 할당되므로 함수가 아니라는 에러를 띄운다.

 

책에서는 함수 선언문이 위치와 관계없이 호출할 수 있어 자유롭지만 위험하다고 말하고 있다.

 

만약에 내가 sayHello함수를 Line:1000에 선언했는데, 다른 개발자가 sayHello함수를 Line:5000에 재선언하면 동일한 변수명에 다른 선언문이 할당되는 것이다.

따라서 가장 나중에 할당한 선언문 Line:5000의 내용이 sayHello 식별자에 저장되고, 먼저 작성된 Line:1000 선언문은 무시된다.

 

Line:5000 이후부터 새로 선언한 sayHello가 실행될거 같지만 결과적으로 모든 코드에 Line:5000의 sayHello가 호출되므로 쉽게 찾을 수 없는 부수효과를 낳을 수 있다.

 

이같은 문제를 방지하기 위해서 전역 공간에 동명의 함수를 선언하는 일은 피해야 한다. 가령 동명 함수가 존재하더라도 이를 함수 표현식으로 작성하는 것을 권장한다.

 

그러나 제일 안전한 방법은 전역 스코프에 함수를 선언하지 말고 지역 변수로 만들어서 외부 스코프에 함수를 은닉시키는 것이다.

 

5. OuterEnvironmentReference, Scope Chain

스코프(Scope)는 자바스크립트 이전의 C, JAVA를 배울 때 부터 빠질 수 없는 개념이다. 자바스크립트도 기존의 개념과 크게 다르지 않다. 어떤 경계 내부에서 선언된 변수는 그 경계 외부에서는 접근할 수 없으며 오직 그 경계 내부에서만 접근이 가능한데 그 때 식별자에 접근 가능한 유효범위를 스코프라고 한다.

 

자바스크립트에서 ES6부터 블록 단위의 스코프가 생성되었다.

let, const, class, strict mode 함수 선언에서만 블록 스코프 단위가 적용된다.

기존의 var는 여전히 함수 스코프에서 작용한다.

 

스코프 체인은 '식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것'을 의미한다.

 

이 스코프 체인과 관련된 정보를 담고 있는 것이 바로 OuterEnvironmentReference이다.

 

위에서 OuterEnvironmentReference는 자신이 선언될 당시의 LexicalEnvironment를 저장한다고 정의했다.

 

여기서 '선언될 당시'라는 표현에 초점을 맞추자. 함수의 선언이 이루어지는 시점은 콜스택에서 어떤 실행 컨텍스트가 존재할 때이다. 따라서 OuterEnvironmentReference에는 자신이 선언될 당시의 콜스택 상의 top에 위치한 실행 컨텍스트의 LexicalEnvironment 정보를 저장한다.

 

코드로 이해해보면, 자신을 감싸고 있는 wrapper 함수 혹은 전역 컨텍스트가 곧 inner 함수의 OuterEnvironmentReference이다.

function A () { // --- outerEnvironmentReference: 전역 컨텍스트
    function B () { // --- outerEnvironmentReference: A
        function C () { // --- outerEnvironmentReference: B
        }
    }
}

 

 

처음 예시코드를 보자.

// --- (0)
var name = 'newjeong'; 

function hello (a) {
    function sayhello () {
        console.log('hello, ' + name); // --- 1.
        var name = 'everyone'; 
    }
    
    sayhello(); // --- (2)
    
    console.log(name); // --- 2.
}

hello(name); // --- (1)
console.log(name); // --- 3.

 

 

처음 예시코드로 이 스코프 체인을 이해하기 위해 name 변수가 어떤 값을 가리키는지 추적하면 된다.

 

컨텍스트 흐름을 보면 1.의 name은 자기자신의 LexicalEnvironment의 name에 매치되어 할당이 되기 전이기 때문에 'hello, undefined'를 출력한다.

 

2.의 name은 먼저 자기 자신의 스코프에 name이 없으므로 바깥 함수를 보게 된다.

hello함수를 선언한 위치가 전역 공간이므로 전역 컨텍스트가 hello 함수의 OuterEnvironmentReference이다. 따라서 전역 변수 name에 매치되어 'newjeong'을 출력한다.

 

3.의 name은 console.log가 매개변수로 전역 변수 name을 받으므로 'newjeong'을 출력한다.

 

1.의 name처럼 inner 함수 내부에 동명의 식별자를 선언하면 외부 변수에는 접근할 수 없게 되는데 이를 변수 은닉화 라고 한다.

 

앞에서 언급했던 동명의 함수 선언으로 인해 이전의 내용을 덮어쓰는 문제를 해결하는 방법 중 가장 안전한 방법으로 전역 스코프에 함수를 선언하지 말고 지역적으로 선언하여 함수를 은닉하는 것이라고 했다. 이 은닉이 곧 변수 은닉화에 의한 것이다.

 

책또한 다음과 같이 지역변수의 사용을 권장한다.

 

이처럼 코드의 안정성을 위해 가급적 전역변수 사용을 최소화하고자 노력하는 것이 좋겠습니다.

 

6. 전역 변수의 최소화하는 방법들

전역변수를 최소화하는 방법으로 책에서 각주로 소개하고 있는 방법들을 더 조사했다.

 

(1) 즉시실행함수 (IIFE)

(_ => {
	console.log("이것은 즉시실행 후 소멸합니다.");
})();

 

 

함수 표현식은 함수명을 따로 명시하지 않고 소괄호로 감싸서 함수를 즉시 실행할 수 있다. 이것을 즉시실행함수(IIFE)라고 한다.

 

즉시실행함수는 함수의 선언과 동시에 호출되므로 실행 컨텍스트또한 콜스택에 push와 실행, 콜스택에서 pop을 원큐로 진행한다. 이렇게 스코프를 제한하여 외부에서의 접근을 차단할 수 있다.

 

(2) namespace

C++ namespace의 개념과 유사하다. 함수나 변수를 특정 변수의 프로퍼티로 저장하는 디자인 패턴을 활용하는 방식이다. 단, 같은 이름의 namespace 역할을 하는 식별자가 존재하는지 우선 확인해야 한다.

 

(3) 모듈 패턴

namespace 패턴을 디자인 패턴으로 확장시킨 개념인 듯 한데 nodejs에서 흔히 쓰고 있던 module.exports가 객체 리터럴을 반환하거나 즉시실행함수를 반환하는 디자인 패턴으로 이미 많이 쓰고 있는 패턴이 곧 모듈 패턴이었다.

 

(4) 샌드박스 패턴

모듈 패턴은 하나의 전역 객체를 리턴했지만, 샌드박스 패턴에서는 생성자를 유일한 전역으로 사용하며 유일한 전역인 생성자에게 콜백함수를 전달해 모든 기능(함수)을 샌드박스 내부 환경으로 격리 시키는 방법이다. 일정한 형식에 따라 샌드박스를 생성할 수 있다.

 

(5) 모듈 관리 도구

자바스크립트의 표준 모듈 체계를 갖추려는 시도들에서 나온 AMD, CommonJS, ES6 등이 존재하는데 좀 오래된 글이지만 개인적으로 가장 쉽게 풀어썼다고 생각되는 링크를 추가하였다.

'Books > 코어 자바스크립트' 카테고리의 다른 글

06. 프로토타입  (0) 2020.04.16
05. 클로저  (0) 2020.04.12
04. 콜백 함수  (0) 2020.04.04
03. this와 객체  (0) 2020.03.05
01. 데이터 타입  (0) 2019.12.31