Execution Context (실행 컨텍스트)

mr.ginger·2021년 10월 2일
0

Execution Context (실행 컨텍스트)

들어가기 전에

실행컨텍스트에 대한 설명을 하기 전에, 우선 StackQue에 대해 먼저 알고 가도록 하자.
자료구조와 알고리즘을 공부했다면 이미 알고 있는 개념이겠지만, 잠깐 다루고 가자면

  • stack은 먼저 들어온 처리 사항을 나중에, 후에 들어온 처리 사항을 먼저 처리하는 자료구조(후입선출)이고,

  • que는 먼저 들어온 내용을 먼저 처리하는 자료구조(선입선출)이다.

이 두 자료구조를 생각하면서, 실행컨텍스트에 대해 알아보도록 하자.

실행컨텍스트란?

실행컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로서, 코드가 실행되기 위해 자바스크립트가 실행하는 특정한 동작들을 의미한다.
즉 자바스크립트가 동작하는 방식이기도 하고, 앞으로 다루게 될 this, hoisting, closure등 여러 개념들과 연결 되는 핵심요소이다.

자바스크립트는 동일한 환경에 있는 코드를 실행할때 필요한 여러가지 환경 정보들을 모아 컨텍스트를 구성하고 call stack에 쌓아올린다. 그렇게 쌓아 올려진 컨텍스트는 위에서부터 차례대로 실행되고, stack의 특성상 그 순서가 보장된다.

여기서 동일한 환경이라는 말이 나왔는데, 이것이 하나하나의 실행컨텍스트를 의미하고, 그 실행컨텍스트를 구성하는 방법으로는 세가지가 있다.

  • 전역코드
  • 함수코드
  • eval코드

여기서 우리가 직접적으로 실행 가능한 코드는 일반적으로, 전역코드와 함수코드이다.
자바스크립트를 실행함과 동시에 생성 되는 전역코드를 제외하고, 가장 많이 사용하는 컨텍스트 구성방법은 함수코드인데, 이는 우리가 함수를 만들어서 실행시킬때 생성 된다.

예제를 하나 들어보자

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

다음과 같은 코드가 있을때 어떠한 순서로 실행컨텍스트가 call stack에 쌓이고, 어떤식으로 처리 될까?

우선 가장 처음 생성되어 call stack에 담기는 컨텍스트는 전역 컨텍스트이다.
전역컨텍스트는 별다른 명령이 없어도 브라우저가 자동으로 실행하기에, 자바스크립트 코드가 구동되면 가장 처음에 생기는 컨텍스트는 전역 컨텍스트이다.

코드를 읽기 시작한 자바스크립트 엔진은 위에서부터 순차적으로 코드를 읽어나가게 된다.
그러다 outer 함수를 만나게 되면, 자바스크립트는 해당 함수에 대한 환경 정보를 수집해서 함수 컨텍스트를 만들고 call stack에 담게 된다.

outer에 대한 컨텍스트가 생성 되면, 전역 컨텍스트에서 함수를 읽던 진행을 멈추고, outer함수와 관련된 코드 즉 outer 내부의 코드들을 실행하게 된다.

같은 원리로, outer에 관한 코드를 실행하다가 inner를 만나게 되면, 다시 진행을 멈추고 inner에 관한 코드를 실행시키게 된다.

이때 하나의 궁금증이 생긴다.
환경 정보를 수집한다는 말이 나왔는데, 환경정보는 무엇을 의미하는것일까?

환경 정보

자바스크립트를 사용하면서 코드를 실행시키기 위해선, 여러가지의 정보가 필요하다.
이 정보는 자바스크립트 엔진이 활용하기 위해 생성 됮는 객체이고, 개발자가 사용 할 수는 없다.

여기에 담기는 정보는 세가지가 있다.

  • Variable Environment
  • Lexical Environment
  • This Binding

하나씩 알아보도록 하자.

Variable Environment

Variable Environment(이하 VE)에 담기는 내용은 기본적으로 Lexical Environment(이하 LE)와 같지만 최초 실행시의 스냅샷을 유지한다는것에 그 차이점이 있다.

VE는 컨텍스트내의 식별자들에 대한 정보와 외부 환경 정보를 담고 있고, 스냅샷이기에 선언 시점의 내용에서 변경 사항은 반영 되지 않는다는 특징이 있다.

최초에 실행컨텍스트가 생성 될때, VE에 그 정보를 담고, 이를 복사해서 LE를 만들고, 이후에는 그대로 LE를 사용하게 된다.
여기서 VE는 변경사항을 반영하지 않기에, 초기화시에는 VE와 LE가 동일하지만, 코드 진행에 따라 달라지게 된다.

VE와 LE의 내부는 environmentRecord(이하 ER)와 outerEnvironmentReference(이하 OER)로 구성되어 있다.

Lexical Environment

LE는 컨텍스트가 생성 되면서 같이 생성된 VE의 정보를 복사한 값을 초기값으로 가지게 되고, LE는 기본적으로 해당 함수가 어떤 변수와 매개변수를 가지고 있고, 어떤 것을 참조하는지 등에 대한 여러가지 정보를 사전에 접하는 느낌으로 모아 놓았다고 할 수 있다.

LE의 내부는 VE와 같이 ER와 OER로 구성되어 있다 하였다.

각각의 구성요소에 대하여 알아보자

EnvironmentRecord & Hoisting

호이스팅에 대해서는 대부분 변수타입 var와 연관이 있다고 알고 있을 것이다.
호이스팅이란 아래에 있는 변수를 끌어올려서(hoist) 할당은 되지 않고, 선언을 시켜 놓는것을 말한다.
그럼 이 hoisting과 ER이 어떤 관련이 있는지, 왜 호이스팅이 일어나는지에 대해 알아보도록 하자.

ER에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 순서대로 수집되어 저장 되게 된다.

이때 변수에 대한 정보들을 수집하였더라도, 아직 해당 컨텍스트의 코드들은 실행 되기 전이다.
마치 시험 시작 직전에 시험지 첫페이지를 매의 눈으로 훑지만, 풀지는 않은 느낌인 것이다.
자바스크립트는 코드가 실행되기전에 코드를 한번 훑고, 해당 코드 내에서 어떤 변수가 있는지 미리 정보를 알고 있게 된다.

즉, 우리가 보기에(실제로 그러지는 않지만) 코드에 있는 변수들을 모두 끌어올려서 해당 변수가 어떤 변수인지 알고, 코드를 시작하는 것을 보고 호이스팅이라 부르는 것이다.
당연히 실행이 되지 않았으므로 해당 변수에 값은 할당 되지 않고, 선언만 되어지게 된다.
이것이 호이스팅이 일어나는 원리이다.

변수의 호이스팅

그렇다면 예제와 함께 알아보도록 하자.

function a(x) {
  console.log(x);
  var x;
  console.log(x);
  var x = 2;
  console.log(x);
}
a(1);

위와 같은 코드가 있을때 각 console.log는 어떻게 찍히게 될까?

여기서 수집대상이 되는 요소를 살펴보면

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

이라 생각 할 수 있다.
물론 실제 자바스크립트 엔진이 이렇게 처리하지는 않고, 이해를 위해 변형 시킨 것이다.
이때 ER은 각각의 어떤 변수가 존재하는지에만 관심이 있고, 각 변수가 어떤 값을 할당 받는지는 관심이 없기에 할당부분은 남겨둔 채로 선언만 수집하게 된다.

수집이 완료 되고 호이스팅이 완료 된 모습을 나타내자면

function a(x) {
  var x;           // 수집대상 1의 선언  
  var x;           // 수집대상 2의 선언
  var x;           // 수집대상 3의 선언
  
  x = 1;           // 수집대상 1의 할당
  console.log(x);  // (1)
  console.log(x);  // (2)
  x = 2;           // 수집대상 3의 할당
  console.log(x);  // (3)
}
a(1);

과 같이 나타 낼 수 있다.

여기서 다른 요소들의 수집이 완료 되면 코드가 실행 되게 되는데, 이때 각 console.log에서 출력 되는 값은 (1) 1,(2) 1,(3) 2 가 나타나게 된다.

수집대상 2에서 undifined가 나오지 않고 1이 출력 되는 이유는 각 변수의 선언이 위로 끌어올려졌고, 변수의 이름이 같았기에 같은 선언으로 간주하고 무시, x에 2가 할당 되기 전까지는 1이 할당 되어 있었기 때문이다.

변수와 함수의 호이스팅

그렇다면 함수가 있으면 어떻게 될까?

코드를 보면서 확인해보자.

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

a 함수를 실행 하는 순간 a 함수의 컨텍스트가 생성 되고, 호이스팅이 일어나게 된다. 변수만 있을때의 호이스팅과는 차이점이 있는데, 변수의 경우는 선언부만 호이스팅이 일어나지만, 함수의 경우 함수 전체가 호이스팅 되게 된다.

즉 호이스팅이 일어나고 난 후의 모습을 보자면

function a() {
  var b;           // 수집대상 1의 선언
  function b(){}   // 수집대상 2 - 함수는 전체가 끌어올려진다.
  
  console.log(b);  // (1)
  b = 'bbb';       // 수집대상 1의 할당
  console.log(b);  // (2)
  console.log(b);  // (3)
}
a();

과 같다고 볼 수 있다.

이 상태에서 각 console.log에선 어떤 값이 나오게 될까?
정답은 (1)b함수, (2)'bbb', (3)'bbb'이다.

호이스팅으로 가장 위에 끌어올려지는데 함수는 전체, 선언과 할당이 동시에 올라가게 되므로 (1)의 b는 함수가 할당이 되게 된다. 그 이후엔 문자열 'bbb'가 할당이 되어 (2),(3)에는 'bbb'가 출력 되게 된다.

OuterEnvironmentReference와 Scope

이번엔 OER에 대해 알아볼 차례다.
이번에 관련이 있는 개념은 바로 스코프이다.

스코프는 식별자에 대한 유효범위를 뜻한다.
예를 들어 A라는 함수가 있을때, A외부에 선언 된 변수는 A안에서 접근 할 수 있지만, A내부에 선언 된 변수는 A외부에서 접근 할 수 없다라는 개념이다.

ES5까지는 자바스크립트에서 전역스코프 외에는 함수로만 스코프를 생성 할 수 있었지만, ES6부터는 함수 뿐 아니라 if등 블록({})으로 스코프를 생성 할 수 있게 되었다.

이러한 스코프를 안에서 바깥으로 찾아 올라가는것을 '스코프 체인'이라 하고, 이 스코프 체인이 가능하게 하는것이 바로 OER인 것이다.

OER은 호출 된 함수가 선언될 당시의 LE를 참조하게 된다.
예를 들어 A함수 내부에 B함수를 선언하고 B함수 내부에 C함수를 선언한 경우, C함수의 OER은 B함수의 LE를 참조하게 된다. 그 이후 B함수->A함수도 마찬가지로 상위의 LE를 참조하게 되는 연결리스트의 형태를 띄게 되고, 마지막에는 전역컨텍스트의 LE를 참조하게 될 것이다.

이렇게 자기 상위의 하나씩만 참조하여 체인을 타고 올라가기 때문에, 체인을 타고 이동하면서 가장 먼저 발견된 식별자에만 접근이 가능하게 된다.

다음 코드를 보며 생각해보자.

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

위 함수의 console.log는 어떻게 출력 될까?

우선 전역컨텍스트가 활성화 되고, ER에 a, outer의 식별자를 저장하게 되고, OER은 비어있게 된다.
그 다음, 코드가 실행 되면서 각 식별자에 값이 할당 되고, outer를 만나 실행을 멈추고, outer의 컨텍스트를 생성시킨다.

outer 컨텍스트가 활성화 된 다음, ER에 식별자 inner가 저장 되고, OER에는 outer가 선언 될 당시의 LE가 기록 되게 된다. 이때 기록 되는 값은 전역컨텍스트의 LE를 참조 복사하게 된다. 그리고 다시 inner를 만나 실행을 멈추고 inner의 컨텍스트가 생성 되게 된다. (OER: [ {GLOBAL, { a, outer } } ])

inner 컨텍스트가 활성화 되면 ER에 식별자 a가 저장 되고, OER에는 inner의 선언 당시의 LE가 기록된다. 이때는 outer의 LE가 참조 복사된다. (OER : [ { outer, { inner } } ])

inner의 실행이 시작 되고, a에 접근 하고자 하였지만, 현재 inner 컨텍스트에 할당된 a의 값이 없기에 undifined가 출력된다. 그 이후 inner 스코프의 a에 3을 할당하게 되고 inner 함수가 종료된다.
이때 inner의 실행컨텍스트가 call stack에서 제거되고, 바로 아래의 outer 컨텍스트가 활성화 되어 outer가 중단 되었던 다음 줄로 이동한다.

outer에서 a에 접근하고자 했을때, outer 안에는 a가 없었기에 체인을 타고 전역으로 이동하게 된다.
이때 전역의 LE에서 할당 되었던 a의 값을 발견하고, 해당 값이 출력 되게 된다. 그 이후 outer의 실행이 종료 되면서 outer의 실행컨텍스트가 call stack에서 제거 되고 전역컨텍스트가 활성화되어 중단되었던 다음줄로 이동한다.

전역컨텍스트의 LE에 할당된 a의 값이 있었기에 그 값이 그대로 출력 되게 된다.
이렇게 모든 코드의 실행이 종료 되고 전역컨텍스트가 call stack에서 제거 되고 종료되게 된다.

만약 inner에서 var a = 3;이라는 코드가 없었다면 inner에서도 1이 출력되었을것이다.
하지만 해당 코드가 존재했기때문에, 호이스팅이 발생했고, a의 선언부가 위로 끌어올려지면서 undefined가 출력되게 된 것이다.
이때 inner 안의 a는 값이 3이 할당 되었지만, 전역과 outer에서는 inner의 변수에 접근 할 수 없기 때문에 기존에 할당 되었던 1이 출력 된 것이다.

때문에, inner에서 선언한 a값때문에 inner에서 전역의 a에는 접근 할 수 없게 되는데, 이를 변수의 은닉화라 한다.


머리가 어지럽지만 열심히 이해해 보도록 하자.

전역변수와 지역변수

위의 스코프 개념을 이용한 전역변수와 지역변수라는 개념이 있다.
전역변수는 말 그대로 전역에서 생성된 변수, 그러니까 전역컨텍스트의 a 변수에 해당하고, 지역변수는 inner 함수 안에서 선언된 a를 이야기한다.
스코프의 특성상 외부에서 내부의 변수에 접근 할 수 없으므로 지역변수로서 선언 된 값은 해당 지역에서만 접근 가능하고, 혹시라도 전역 공간에서 변수의 이름이 겹쳐 덮어쓸(overriding)될 위험을 줄일 수 있다.
그렇기에 가능하면 전역변수가 아닌 지역변수를 사용하도록 하자.

this

실행컨텍스트의 thisBinding에는 this로 지정된 객체가 저장 되게 된다.
컨텍스트가 활성화 될때, this가 지정 되지 않은 경우엔 전역객체가 저장 되게 된다.
그 외에는 함수를 호출하는 방법에 따라서 this의 값이 바뀌게 된다.
this에 대해서는 다음 포스팅에서 다루도록 하겠다.


해당 포스팅은 코어자바스크립트 2장 실행컨텍스트를 바탕으로 포스팅하였습니다.

0개의 댓글