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

Doodream·2021년 3월 21일
1

코어 자바스크립트

목록 보기
1/36
post-thumbnail

실행 컨텍스트

실행 컨텍스트동일한 환경의 코드들이 실행될때 해당하는 코드가 돌아가는 환경을 구성해놓은 객체 이다. 이러한 객체인 실행컨텍스트를 구성하는 방법으로서는 함수, 전역공간,악마로 취급받는 eval()함수등이 있는데, 자바스크립트의 전체코드의 환경과 순서는 stack 방식으로서 실행 컨텍스트가 쌓여있고 맨위에 있는 실행 컨텍스트 부터 실행되는 방식이다.

대부분 가장 흔한 형태로서 실행컨텍스트를 구성하는 방법으로 함수를 많이 사용한다.

전역컨텍스트

처음 자바스크립트 코드를 실행하는 순간 전역컨텍스트가 콜 스택에 담긴다. 무슨이야기냐 하면 전역컨텍스트는 일반적인 실행컨텍스트와 별 다를게 없다. 다만, 최상단의 코드에 아무것도 없어도 브라우저에서 알아서 실행시켜주기 때문에 자바스크립트 파일이 브라우저에서 열리는 순간 전역컨텍스트가 콜 스택에 담기며 전역컨텍스트가 활성화된다.

❓ 콜스택 : 자바스크립트가 실행해야할 코드 환경(실행컨텍스트)가 담길 stack 이라고 생각하면서 자바스크립트가 코드를 실행하면서 다른 환경의 실행컨텍스트를 만나면 콜스택에 넣고 코드의 실행순서를 보장한다.

코드의 실행순서

var a = 1;
funciton outer() {
  function inner() {
    console.log(a);
    var a =3;
  }
  inner();
  console.log(a);
}

outer();
console.log(a);
  1. 자바스크립트 실행전 콜스택은 비어있다.
  2. 이후 자바스크립트 코드를 실행하자마자 전역컨텍스트가 콜스택에 담긴다. 전역컨텍스트외 다른 코드는 없으므로 해당 코드들을 순차적으로 실행하다가
  3. outer() 함수(다른 환경의 실행컨텍스트)를 만나고 잠시 전역컨텍스트의 실행을 멈추고 outer()함수의 환경을 수집해서 실행컨텍스트를 만들어 콜스택에 쌓아둔다.
  4. outer() 함수 안의 코드를 실행하기 시작한다. 이때 여전히 전역컨텍스트에 해당하는 코드들은 실행이 멈춰있다.
  5. outer() 코드를 실행중 inner()함수를 만나면 다시 outer() 코드실행을 중단하고 inner()코드의 환경정보를 수집하여 실행컨텍스트를 구성한다.
  6. inner()코드를 실행하고 모두 실행되었으면 콜스택에서 제거한다. 이후 콜스택의 최상단의 outer()코드가 실행되게 된다.
  7. outer()코드가 모두 실행되면 콜스택에서 제거되고 가장 최상단의 전역컨텍스트가 실행된다.
  8. 전역컨텍스트 코드가 모두 실행되면 마찬가지고 콜스택에서 제거되고 아무것도 남지 않은 콜스택의 상태로 종료가된다.

이러한 과정을 자바스크립트 엔진이 실행한다.

ex) 구글 Chrome V8버전 JS엔진
node js는 이러한 엔진으로 만들어진 런타임 구동 플렛폼이다.

실행 컨텍스트의 수집정보

이러한 실행컨텍스트 객체는 자바스크립트 엔진이 코드를 실행활 환경을 구축하는데 사용될 뿐으로 개발자가 코드를 통해 확인이 불가능하다. 여기에 담기는 정보들은 다음과 같다.

  • VariableEnvironment
    현재 컨텍스트내의 식별자들에 대한 정보 + 외부 환경 정보,
    선언시점의 LexicalEnvironment의 스냅샷으로 변경사항은 반영되지 않음.

  • LexicalEnvironment
    처음에는 VariableEnvironment와 같지만 변경사항이 실시간으로 반영

  • ThisBinding
    식별자가 바라봐야 할 대상 객체

VariableEnvironment

실행컨텍스트를 생성할 때 VariableEnvironment에 이러한 정보들이 담기게 됩니다.

현재 컨텍스트에는 a라는 식별자가 있고 그 외부정보로는 D를 참조하도록 구성되어 있다.

처음에는 VariableEnvironment에 이러한 정보들이 담기고 나서 이를 복사해서 LexicalEnvironment를 구성하고 이후에는 이것을 주로 활용하게 된다. (실시간으로 반영하기 때문에)

LexicalEnvironment

Lexical이란 의미는 사전적인 이라는 의미로 이해하면 좋다. 여기에는 현재 컨텍스트의 식별자 a, b, c가 있으며 그 외부정보로는 D, T, K를 참고 한다는 형태이므로 이것은 마치 사전에 바나나라는 식별자는 외부정보로서 '알칼리석 식품으로서 칼륨 카로틴 비타민 c를 함유하고 있다.'라는 정보를 참고 하듯이 말이다.

어떠한 용어를 대할때 개인의 이해를 위한 용어와 타인과의 커뮤니케이션을 위한 용어를 구분해야한다. 위의 겅우 타인과의 커뮤니케이션을 위한 용어를 사용하려면 원어 그대로 lexicalEnvironment라는 단어를 사용해야한다.

environmentRecord와 호이스팅

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.

예를 들면 컨텍스트를 구성하는 함수코드에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등이 식별자에 해당한다. 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다.

✔️ 전역 실행 컨텍스트는 일반 적인 실행 컨텍스트와는 달리 변수의 식별자들을 수집하는게 아니라 자바스크립트 구동환경(구글 크롬 V8, node js등등)에서 별도로 제공하는 전역객체를 활용한다. 이러한 전역객체는 브라우저의 window, node js의 global객체등이 있다. 이러한 객체들은 자바스크립트의 내장객체가 아닌 호스트 객체로 분류된다.

호이스팅

자바스크립트 엔진은 실행컨텍스트에 해당하는 코드들은 여전히 실행전인데도 불구하고 실행컨텍스트를 구축(환경정보 → 식별자들의 정보(변수명 같은)) 함으로서 이미 해당하는 코드의 변수명들을 모두 알고 있는 셈 입니다.

즉, 자바스크립트 엔진은 식별자들을 최상단으로 끌어올린다음 실제 코드를 실행한다.

라고 봐도 무방하다. 여기서 호이스팅의 개념이 등장하는데 실제로는 식별자들의 정보를 끌어올린 상태에서 실행하진 않지만 그렇게 봐도 무방하다는 가상의 개념이다.

호이스팅 규칙

function a () {
  console.log(b); // 1
  var b = 'bbb';
  console.log(b); // 2
  function b() {};
  console.log(b); // 3
}
a();

위코드를 예를 들어 설명해보자. 함수안에 들어있는 매개변수들은 LexicalEnvironment(식별자 정보)들을 수집할때 함께 수집된다. 즉 인자는 함수 내부를 실행하기전 실행컨텍스트로 수집함으로서 호이스팅 된다. (끌어올려진다) 라고 생각될 수 있다. 하지만 식별자 자체(변수명)만 끌어올려지고 값을 할당하는 과정은 해당 코드에서 따라서 이렇게 코드를 변형시켜볼수 있다.

아래 코드는 사람이 이해하기 쉽게 코드 순서를 변형한 것이지 실제 동작방식은 아니다.

function a () {
  var b; // 1의 매개변수
  var b = function b () {};// 2의 함수 선언부분
  
  console.log(b); // 1
  b = 'bbb';
  console.log(b); // 2
  console.log(b); // 3
}
a();
  1. a함수가 실행되면 함수 선언부 전체가 실행컨텍스트에 올라가고 콜스텍에 추가된다.

  2. 실행컨텍스트의 내부코드를 차례로 실행할 차례이다. 먼저 변수 선언부가 호이스팅 된다. 1의 매개변수의 b로서 변수 b를 선언하여 메모리에 저장할 공간을 미리 확보하고 확보한 공간의 주솟값을 b에 연결해둔다.

  3. 다시 변수 b를 선언하고 함수 b를 선언된 변수 b에 할당하라한다. 변수 b는 이미 선언되어 메모리 확보가 되었으므로 무시하고 함수 b가 선언된 메모리 주소값을 b와 연결된 공간에 저장한다. 이제 변수 b는 함수 b를 가리키게 된다.

  4. 1번 출력은 변수 b에 할당된 값을 출력하려고한다.

  5. 변수 b에 연결된 공간에 'bbb'를 저장한 값의 주소값을 넣는다. 이제 변수 b는 'bbb'를 가리킨다.

  6. 2, 3번 출력에서는 'bbb'를 출력한다.

호이스팅을 알기전에는
순서대로 undefined, 'bbb', b함수 가 출력될줄 알았으나 실제로는 b함수, 'bbb', 'bbb' 라는 전혀 다른 결과가 나왔다.

함수 선언문과 함수 표현식

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

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

var c = function d () {}// 기명 함수 표현식
c();
d(); //error
  • 함수 선언문 : 함수선언식 자체이다.

  • 함수 표현식 : 변수를 선언해서 함수를 선언한다.

    1. 익명 함수 표현식 : 함수 자체에 이름이 없다. 하지만 함수 자체 내부코드에서 b라는 변수명을 호출해서 함수를 호출해도 자동으로 함수명에 할당해서 기명함수 표현식과 같다.

    2. 기명 함수 표현식 : 함수를 호출하는 변수명이 존재하지만 함수명이 따로 있다. 함수 자체 내부코드에서 변수명으로 함수를 호출 하든 함수명으로 호출하든 둘다 호출이 가능하지만 외부에서는 함수명으로 호출시 호출이 되지 않는다.

함수 선언문과 함수 표현식의 호이스팅 차이

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;
}

이제 위코드를 호이스팅하여 실제 코드가 처리되는 순으로 바꿔보자

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;
}
  1. 함수 선언부 전체가 호이스팅되어 가장 위에 있다.
  2. 변수 multiply가 선언되어 메모리가 할당된다.
  3. sum 함수가 실행되어 3의 결과가 나온다.
  4. 변수 multiply에는 아직 아무런 데이터가 저장되어있지 않으므로 undefined가 출력된다.
  5. multiply 변수에 곱셈 함수가 연결된다.

이렇게 보듯이 함수 선언부와 함수 표현식의 차이를 자세히 짚어보자.

  • 함수 선언문은 함수명이 같다면 코드전체에서 가장 마지막에 정의한 함수가 코드 전체에 걸쳐 실행된다. 매우 중요한 포인트이다.
function sum (a, b){// 함수 선언문
  return a + b;
}

console.log(sum(1,2));

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

console.log(sum(1,2));
}

→ 호이스팅 변환 후

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

function sum (a, b){// 함수 선언문 다시 sum이라는 함수가 재 선언되었다. 
  return a + 2 * b;
}
console.log(sum(1,2)); // 5출력
console.log(sum(1,2)); // 5출력
}
  • 함수 표현식은 함수가 정의된 부분이 그위치에서 그대로 선언이 되기때문에 같은 변수명이더라도 그 순서대로 정의되어 실행된다.
var multiply = function (a, b) {// 함수 표현식
  return a * b;
}

console.log(multiply(3, 4));

multiply = function(a, b){
  return a * b * a;
  
console.log(multiply(3, 4));

→ 호이스팅 변환 후

var multiply; 

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

console.log(multiply(3, 4));

multiply = function(a, b){
  return a * b * a;
  
console.log(multiply(3, 4));

이렇듯 함수 선언문 보다는 함수 표현식이 훨씬 더 안정적이다. 따라서 코드를 작성할 때에는 전역공간에 함수 선언을 한다거나 같은 변수명으로 함수 선언문을 작성하는 것은 협업의 입장에서 봤을 때 굉장히 불안한 코드라고 할수 있다.

비교적 함수 표현식이 안전하다.

const sleepAlarm = () => {}

리액트나 다른 컴포넌트 형태의 코드에서도 코드 안정성을 위해서 저렇게 함수를 작성한다.

스코프, 스코프 체인, outerEnvironmentReference

스코프

스코프란 식별자에 대한 유효 범위로서 식별자가 어떠한 범위에서 선언이 되어 가용한지에 대한 범위이다. 즉, A라는 경계안에서 선언한 식별자는 A라는 경계안에서만 유효하며 그 밖의 경계에서는 유효하지 않다는다는 개념이 스코프이다.

스코프체인

이러한 식별자의 유효범위를 경계들의 안쪽에서 바깥쪽으로 검색해 나가는 것을 스코프 체인이라는 개념이라고 합니다. 정밀하게 말하자면 다음과 같습니다.

먼저 어떤 식별자가 선언되는 코드를 실행할 때에는 당연하게도 해당 코드의 실행컨텍스트가 콜스택상에서 활성화 되어있을 때 가능합니다.

A함수 안에 B함수가 선언되고 그안에 C함수가 선언되었다고 한다면 C함수의 실행컨텍스트 안의 LexicalEnvironment는 그 두번째 수집자료인 outerEnvironmentReference을 참고합니다. 이 outerEnvironmentReference는 B함수의 실행 컨텍스트 안의 LexicalEnvironment를 참고합니다. 다시 이정보는 A함수의 실행컨텍스트안의 LexicalEnvironent를 참고하고 다시 이정보는 전역공간의 실행컨텍스트 LexicalEnvironment를 참고하게됩니다. 이렇게 연결리스트 형태로 정보가 이어진 것을 스코프 체인이라고 합니다.

outerEnvironmentReference

LexicalEnvironment의 두번째 수집자료로서 해당 실행컨텍스트가 선언했을 당시의 실행컨텍스트의 LexicalEnvironment를 참조합니다. 즉, 부모 실행컨텍스트의 LexicalEnvironment의 정보가 자식 컨텍스트의 outerEnvironmentReference에 참조됩니다.

따라서 여러 스코프상에서 같은 이름의 변수가 선언되었다면 이러한 스코프 체인내에서 가장 먼저 접근되는 식별자로 접근되는 것 입니다. 예를 들어보자면

var a = 1;
var outer = function() {
  var inner = function() {
    console.log(a);
    var a = 3;
  };
  inner();
  console.log(a);
};
outer();
console.log(a);

위코드의 실행순서를 알아보자. → 알아보기 위해서 호이스팅 순서대로 재배치하면

var a;
var outer;

a = 1;
outer = function() {
  var inner; 
  inner = function() {
    var a;
    console.log(a);
    a = 3;
  };
  inner();
  console.log(a);
};
outer();
console.log(a);
  1. 변수 a, outer가 전역실행컨텍스트의 environmentRecord에 배치되며 그중에서도 VariableEnvironment에 생성되서 LexicalEnvironment에 복사됩니다.
    그안의outerEnvironmentReference에는 아무것도 담기지 않습니다. 전역 컨텍스트가 선언될 당시의 실행컨텍스트가 없기 때문입니다.

  2. a와 outer에 각각 1과 함수가 할당되고 outer함수가 실행됩니다. 이에 따라 전역컨텍스트에 해당하는 코드는 잠시 멈추고 outer 함수의 실행컨텍스트가 구성됩니다. 마찬가지로 outer 함수의 LexicalEnvironment안의 outerEnvironmentReference는 전역컨텍스트의 LexicalEnvironment를 참조합니다.

  3. inner 변수명이 outer함수의 environmentRecord에 담기게 됩니다.
    inner에 함수가 할당되고 inner함수가 실행됩니다. 이에 따라 outer함수 코드는 잠시 실행을 멈추고 inner함수의 실행컨텍스트가 구성됩니다.

  4. inner 함수의 실행컨텍스트의 environmentRecord에 변수 a가 담기게 되고 LexicalEnvironment 안의 outerEnvironmentReference에는 outer함수의 LexicalEnvironment를 참조하게 됩니다.

  5. inner 함수안에서 console.log(a) 가 실행되어 식별자 a가 접근하려고 합니다. inner함수 안의 environmentRecord에서 식별자 a를 검색해서 발견되었습니다. 해당 스코프내에서는 a가 선언은 되었지만 할당된 데이터가 없으므로 undefined 가 출력됩니다. 변수 a에 3이 할당됩니다.

  6. inner함수의 코드가 끝나고 inner 함수의 실행컨텍스트가 콜스텍에서 제거됩니다. 다시 outer() 함수가 멈췄던 console.log(a) 가 실행됩니다. 식별자 a에 접근하기 위해서 outer함수의 environmentRecord에서 a를 검색합니다. 발견되지 않아서 outerEnvironmentReference에 참조되어 있는 전역 컨텍스트의 LexicalEnvironmentenvironmentRecord에서 식별자 a를 검색합니다. 발견되었고 a는 1이 할당되어 console.log(a)는 1을 출력합니다. (❗️ 스코프 체인)

  7. outer함수가 종료되고 outer 함수의 실행컨텍스트가 콜스텍에서 제거됩니다. 다시 멈췄던 전역컨텍스트에 해당하는 코드가 실행되고 console.log(a)가 실행됩니다. 식별자 a에 접근하기 위해서 전역컨텍스트의 environmentRecord에서 식별자 a를 검색하고 발견합니다. a는 1이 할당되어 1을 출력합니다.

  8. 코드가 모두 실행되어 콜스텍에서 전역컨텍스트를 제거하고 비게됩니다.

즉, 가장 깊은 곳에 있는 inner함수에서는 전역컨텍스트, outer 함수의 컨텍스트, inner 함수의 컨텍스트에 있는 environmentEnvironment에 접근할수 있습니다. 이러한 검색과정을 스코프 체인이라고 합니다.

변수의 은닉화

이러한 과정에서 여러 스코프내에서 동일한 변수명을 가진 변수가 발견된다면 스코프체인 검색과정에서 가장 먼저 발견된 변수가 출력됩니다. 이러한 특성을 변수의 은닉화 라고 합니다.

구글 크롬에서의 스코프체인 확인

var a = 1;
var outer = function () {
    var b = 2;
    var inner = function () {
        console.dir(inner);
    };
    inner();
}
outer();


구글크롬에서는 위와같이 console.dir()함수를 통해서 해당 함수의 스코프 체인을 확인할 수 있는데 inner함수는 전역컨텍스트와 outer()함수의 실행컨텍스트를 참조 할수 있다는 것을 보여줍니다. 더 자세한 확인 위해서는 console.dir()부분을 debugger로 바꿔서 하나씩 확인해봅니다.

vscode에서 실행하면 다음과 같이하나씩 넘어가면서 콜스택과 변수값들을 실시간으로 확인이 가능합니다.

thisBinding

실행 컨텍스트의 수집정보 세번째로 thisBinding이 있습니다. 이정보에는 this에 지정된 객체가 담겨지는데 실행컨텍스트 구성당시 this에 지정된 객체가 없다면 전역객체(window, document.. )가 담기게됩니다. this는 다음 챕터에서 자세히 배우도록 합시다.!

profile
일상을 기록하는 삶을 사는 개발자 ✒️ #front_end 💻

0개의 댓글