본문 바로가기

개발자 페이지/Javascript

02 실행 컨텍스트 - by 코어자바스크립트

728x90
반응형

아래 내용은 '코어 자바스크립트' 정재남 저 | 위키북스의 내용을 발췌한 것으로

 자세한 내용은 해당 서적을 확인 바랍니다. 

 

실행 컨텍스트(Execution context): 실행할 코드에 제공할 환경 정보들을 모아놓은 객체. 

 

자바스크립트는 실행컨텍스트가 활성화되는 순간 다음과 같은 동작을 수행하게 된다.

1. 호이스팅(Hoisting): 변수를 위로 끌어올림

2. 외부 환경 정보를 구성

3. this 값을 설정하는 등의 동작 수행

 

해당 개념은 JS에서 가장 중요한 핵심 개념중 하나로, 실행컨텍스트(Execution context)를 정확히 이해하는 것은

자바스크립트를 포함, 개발자라면 반드시 숙지해야 할 핵심 개념이다.

Stack & Queue

 

- 스택(stack): 출입구가 하나인 깊은 구덩이 데이터 구조

 비어있는 스택순서대로 LIFO(Last In First Out) 의 개념을 지는 데이터 구조다. 

100개만 넣을수 있는 이 스택에 그 이상이 들어가면 많은 프로그래밍언어가 에러를 출력한다.

"Uncaught RangeError: Maximum call stack size exceed..." 

Stack

 

 

- 큐(queue): 양쪽이 모두 열려있는 파이프 데이터 구조

 종류에 따라 양방향 모두 입,출력이 가능한 큐도 있으나, 보통 한쪽은 Input, 다른 쪽은 Output만을 담당하는 구조다.

FIFO(First In First Out) 의 개념을 가지는 데이터 구조이다.

Queue

 

보통 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것이다.

 

JS 코드를 실행하는 순간 전역 컨텍스트가 콜 스택에 담기며, 전역 컨텍스트는 실행 컨텍스트라는 개념과 특별히 다를 것이 없다. 최상단의 공간은 코드 실행 명령이 없어도 브라우저에서 자동으로 실행하므로, 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화 된다고 이해하면 된다.

 

스택 구조는 실행 컨텍스트가 콜 스택 맨 위에 쌓이는 순간이 현재 실행할 코드임을 의미한다.

이렇게 실행 컨텍스트가 활성화 될 때 JS 엔진은 해당 context에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장을 한다. 이 객체는 JS 엔진이 활용할 목적으로 생성하는 것으로 개발자가 코드를 통해 확인할 수 없으며, 그 정보는 다음과 같다.

 

1. VariableEnvironment: 현재 Context 내의 identifier (식별자)에 대한 정보 + 외부 환경 정보 + 선언시점 LexicalEnvironment의 스냅샷으로, 변경사항 저장X.

 

 최초 실행시의 스냅샷을 유지함.  실행 컨텍스트를 생성시 ViriableEnvironment에 먼저 정보를 담고, 이를 그대로 복사하여

LexicalEnvironment를 만들고, 이후 LexicalEnvironment를 활용한다.

 

VariableEnvironment 

LexicalEnvironment  이 둘의 내부는

environmentRecord 와 outerEnvironmentReference로 구성돼 있다.

(초기화 과정 중에는 사실 완전히 동일, 이후 코드 진행에 따라 서로 달라지게 됨)

 

2. LexicalEnvironment: 초기에 VairableEnvironment와 같으나 변동사항 실시간 저장&반영

 사전적 의미로 어휘적환경, 정적 환경 등으로 자주 번역된다.

 

environmentRecoer&호이스팅 - 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장.

context를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 

변수의 식별자 등이 식별자에 해당한다. Context 내부 전체를 처음부터 끝까지 훑어가며 순서대로 수집함.

 

◈전역 실행 컨텍스트는 변수객체를 생성하는 대신 JS 구동 환경이 별도로 제공하는 Obj, 즉 Global Object를 활용한다.

전역 객체(global object)에는 브라우저의 window, Node.js의 global 객체 등이 있다. 이들은 JS 내장 객체(native obj)가 아닌 호스트 객체(host object)로 분류된다.

 

코드가 실행되기 전에 이미 JS engine은 해당 환경에 속한 코드의 변수명들을 모두 알고 있는 셈이다.

엔진의 실제 동작 방식 대신에 'JS engine은 식별자(identifier)들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다' 라고 생각해도 코드를 해석하는데 문제 될 것이 전혀 없다. 여기서 호이스팅(hoisting)개념이 등장하게 된다. 

 

호이스팅 규칙 

environmentRecord에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다고 하였다.

이 environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 

어떤 값이 할당될 것인지는 관심이 없다. 따라서 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨두게 된다. 

function a () {
	var x = 1;          //수집대상1 (매개변수 선언)
    console.log(x);
    var x;            //수집대상2 (변수 선언)
    console.log(x);
    var x = 2;
    console.log(x);   //수집대상3 (변수 선언)
    
}
a();

위 console.log(x) 결과 값은 차례로 1,1,2 라는 결과 값을 출력하게 된다.

호이스팅 개념을 정확히 이해하지 못하면 예측하기 어려운 결과이다.

 

+ 함수선언문(function declaration), 함수표현식(function expression)

둘 모두 함수를 새롭게 정의할 때 쓰이는 방식이며, 함수 선언문은 function 정의문만 존재하고 별도의 할당 명령이 없는것을 의미한다. 반대로 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말한다. 

 

함수 선언문의 경우 반드시 함수명이 정의돼 있어야 하지만, 함수 표현식은 없어도 된다.

함수명을 정의한 함수 표현식을 '기명 함수 표현식', 정의하지 않은 것을 '익명 함수 표현식'이라 부르기도 하지만,

일반적으로 함수 표현식은 익명 함수 표현식을 말한다.

function sum (a, b) {    // 함수 선언문 sum
	return a + b;
}

var multiply = function (a, b) {  //함수 표현식 multiply
	return a * b;
}

함수 선언문은 전체를 호이스팅하는 반면, 함수 표현식은 변수 선언부만 호이스팅한다. 

 

함수도 하나의 값으로 취급할 수 있다는 것은 바로 이런것이다.

함수를 다른 변수에 값으로써 '할당'하는 것이 곧 함수 표현식이다.

 

전역 컨텍스트가 활성화 될 때 전역공간에 선언된 함수들이 모두 가장 위로 끌어올려지며, 동일한 변수명에 서로 

다른 값을 할당할 경우 나중에 할당한 값이 먼저 할당한 값을 덮어씌우게 된다. 따라서 코드를 실행하는 중에 실제로 호출되는 함수는 오직 마지막에 할당한 함수, 즉 맨 마지막에 선언된 함수뿐이다.

100번째 줄에 x+y 함수를 선언한 뒤

5000번째 줄에 x+y = (x+y)문자로 선언하는 코드를 작성한다면

100번째 함수 결과 값은 5000번째 결과값으로 반환될 것이다. 

 

여기서 아무런 오류 없이 통과되기에 뭐가 문제인지 도통 알 수가 없게된다. 

회사는 버그를 수정하라고 압박인데 어떤 코드가 문제인지를 어디서부터 어떻게 찾아야 할지 엄두가 나지 않게 된다. 

함수 표현식으로 정의했다면 의도대로 잘 작동했을것이고, 에러도 바로 검출되기에 빠르게 디버깅 할 수 있었을 것이다.

 

'상대적으로 함수 표현식이 안전하다'

원활한 협업을 위해서는 전역공간에 함수를 선언하거나 동명의 함수를 중복 선언하는 경우는 없어야만 한다. 

그러나 만에 하나 전역공간에 동명의 함수가 여럿 존재하는 상황이라 하더라도 모든 함수가 함수 표현식으로 정의돼어 있었다면 위와 같은 상황은 일어나지 않을 것이다. 

 

스코프, 스코프체인, outerEnvironmentReference

스코프(scope)는 식별자에 대한 유효범위이다. 

A 외부에서 선언한 변수는 A외부 뿐만 아니라 A 내부에서도 접근이 가능하지만,

A 내부에서 선언한 변수는 오직 A의 내부에서만 접근 가능하다. 

이러한 Scope의 개념은 대부분의 언어에 존재한다.

 

이런 '식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인(scope chain)이라고 한다.

그리고 이를 가능케 하는 것은 LexicalEnrivonment의 두번째 수집 자료인 outerEnvironmentReference 이다.

 

스코프체인 

outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.

어떤 함수를 선언(정의)하는 행위 자체도 하나의 코드에 지나지 않으며, 모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행된다.

 

ex) A 함수 내부에 B 함수를 선언하고 다시 B 함수 내부에 C 함수를 선언한 경우, 

함수 C의 outerEnvironmentReference는 B의 LexicalEnrivonment를 참조하게 된다. 

함수 B의 LexicalEnvironment에 있는 outerEnvironmentReference는 다시 함수 B가 선언되던 때

(A)의 LexicalEnvironment를 참조하게 된다. 이처럼 outerEnvironmentReference는 연결리스트(linked list)형태를 띈다.

 

선언 시점의 LexicalEnvironment를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnvironment가 있게된다. 

또한 각 outerEnvironmentReference는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있으므로 가장 가까운

요소부터 차례대로 접근할 수 있고 다른 순서로 접근하는 것은 불가능할 것이다.이러한 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 된다.

 

코드가 처음 실행되면 

1. 전역 컨텍스트 활성화

2. 전역 컨텍스트의 environmentRecord에 식별자(identifier)저장, 이때 선언 시점이 없으므로 outerEnvironmentReference에는 아무것도 담기지 않는다.(this: 전역 객체)

예제) Scope chain

위 결과값은

undefined, 1, 1 순으로 출력 됨

 

※ 전역 컨텍스트 -> outer -> inner 로 갈수록 규모가 작아지는 반면, 스코프 체인을 타고 접근 가능 변수는 늘어난다.

전역공간에서는 전역 스코프에 생성된 변수에만 접근 가능

outr 함수 내부에서는 outer 및 전역 스코프에서 생성된 변수에 접근할 수 있지만 inner 스코프 내부에는 접근 못한다.

inner 함수 내부에서는 inner, outer, 전역 스코프 모두에 접근할 수 있다.

 

*스코프 체인 상에 있다고 모두 접근가능한 것은 아니며, inner 내부에서 a에 접근하려면 무조건 스코프 체인 상 첫 번째 인자인 inner 스코프의 LexicalEnvironment 부터 검색하게 된다. 

inner 스코프 LexicalEnvironment에 a 식별자가 존재하므로 스코프 체인 검색이 더 진행되지 않고 즉시 inner LexicalEnvironment 상의 a를 반환하게 된다. 즉 inner 함수 내부에서 a 변수를 선언했기 때문에 전역공간에서 선언한 돌일한 이름의 a변수에는 접근할 수 없는 셈이다.

이를 변수 은닉화(variable shadowing) 하고 한다.

 

◈전역변수(global variable)와 지역변수(local variable)

전역변수: 전역 공간에서 선언한 변수

지역변수: 함수 내부에서 선언한 변수

 

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

global 에서는 local 공간에 있는 함수에 접근할  수 없게된다. 

 

 

3. ThisBinding: 식별자(indentifier)가 바라봐야 할 대상 객체

 

실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다.

실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역객체가 저장되며, 그 밖에 함수를 호출하는 방법에 따라 this에 저장되는 대상이 달라지게 된다.

 

Recap 

 

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

2. 실행 컨텍스트 객체는 활성화 시점에 VariableEnvironment, LexicalEnvironment, ThisBinding 의 세 가지 정보를 수집.

3. Variable E, Lexical E 동일 내용으로 구성되지만

- Lexical E은 함수 실행 도중 변경사항 즉시 반영

- Variable E는 초기 상태 유지

4. VariableE, LexicalE는 매개변수명, 변수의 식별자, 선언한 함수의 함수명등을 수집하는 environmentRecord와 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성돼 있다.

5.호이스팅은 코드 해석을 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념.

변수 선언과 값 할당이 동시에 이뤄진 문장은 '선언부'만을 호이스팅하고, 할당 과정은 원래 자리에 남아있게 되는데, 여기서 함수 선언문과 함수 표현식의 차이가 발생한다.

6. 스코프는 변수의 유효범위를 말한다. 

outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조한다. 

코드 상에서 특정 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnvironment를 탐색 후 발견되면 그 값을 반환하고, 발견하지 못하면 다시 outerEnvironmentReference까지 탐색하며 그래도 해당 변수를 찾지 못하면 undefined를 반환한다.

 

전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 하고, 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들은 모두 지역변수이다. 협업을 최적화 하기 위해 전역변수의 사용은 최소화 하는 것이 바람직하다.

this에는 실행 컨텍스트를 활성화하는 당시에 지정된 this가 저장되며, 함수를 호출하는 방법에 따라 그 값이 달라지는데, 지정되지 않은 경우에는 전역 객체가 저장된다.

728x90