12. 실행컨텍스트

 

12-1. 실행 컨텍스트란?

실행 컨텍스트는 JavaScript에서 코드가 실행되는 환경을 의미한다. 이 환경은 변수, 함수 선언, 스코프, this값 등을 포함한다. 추가적으로 실행 컨텍스트는 코드의 실행을 위해 필요한 정보를 수집하고 관리하는 역할을 한다.

12-1-1. 정의와 개념

컨텍스트는 한국말로 번역하면 문맥이다. 쉽게 코드의 실행 환경이라고 이해하면 된다. 컨텍스트는 코드의 실행 순서와 범위를 결정하는 데 중요한 역할을 한다. 실행 컨텍스트는 스택 형태로 관리되며, 함수가 호출될 때마다 새로운 컨텍스트가 생성된다. 각 컨텍스트는 독립적으로 관리되며, 변수와 함수의 식별자를 저장하고 상위 스코프와의 관계를 유지한다.

12-1-2. 실행 컨텍스트의 필요성

실행 컨텍스트는 자바스크립트가 왜 그렇게 동작 하는지 설명해 준다. 개념이 복잡하긴 하지만 자바스크립트의 동작 원리에 대해 아주 큰 역할을 차지하고 있기 때문에 이해하고 넘어가야 하는 부분이다. lexical scoping과 실행컨텍스트 부분만 이해하면 자바스크립트를 이해하는 데 필수적인 개념인 호이스팅, 클로저까지 모두 쉽게 이해가 가능하다.

12-1-3. 렉시컬 스코핑(lexical scoping)

본격적인 설명 전 렉시컬 스코핑에 대해서 정확하게 알고 넘어가야 실행컨텍스트의 전반적인 내용을 이해하기에 적절하다. 많이 헷갈릴 수 있는 개념인데 스코프는 함수를 호출할 때가 아니라 선언할 때 생긴다. 호출이 아니라 선언임을 기억해야 한다.
이는 정적 스코프라고도 불리는데, 다음 코드에서 console이 어떻게 찍힐지 예상해 보자.
var name = 'Kim'; function log() { console.log(name); } function wrapper() { name = 'Park'; log(); } wrapper(); // park
console에 출력된 값을 살펴보면. log를 호출하기 전에 name을 ‘Park’으로 바꿨기 때문이다.
다음 코드도 console이 어떻게 찍힐지 예상해 보자.
var name = 'Kim'; function log() { console.log(name); } function wrapper() { var name = 'Park'; log(); } wrapper(); // kim
똑같이 Park이라고 예상했으면 스코프를 다시 공부해야 한다.
스코프는 함수를 선언할 때 생긴다고 했으므로, log 안의 name은 wrapper안의 지역변수 name이 아니라, 전역변수 name을 가리키고 있는 것이다. 이를 lexical scoping이라고 한다. 함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 스코프로부터 가장 가까운 곳에 있는 변수를 계속 참조하게 된다. 위의 예시에서는 log 함수 안의 name 변수는 선언 시 가장 가까운 전역변수 name을 참조하게 된다. 그래서 wrapper 안에서 log를 호출해도 지역변수 name=’Park'를 참조하는 게 아니라 그대로 전역변수 name의 값인 ‘Kim’이 나오는 것이다. console에 찍히는 name을 바꿀 수 있는 방법은 예제 1처럼 전역변수에 있는 name의 값을 함수 스코프 안에서 전역변수로써 바꿔주는 것이다.

12-1-4. 실행 컨텍스트의 원칙

컨텍스트에는 4가지 원칙이 따른다.
  1. 먼저 전역 컨텍스트 하나 생성 후, 함수 호출 시마다 컨텍스트가 생긴다.
  1. 컨텍스트 생성 시 컨텍스트 안에 변수 객체(arguments, variable), scope chain, this가 생성된다.
  1. 컨텍스트 생성 후 함수가 실행되는데, 사용되는 변수들은 변수 객체 안에서 값을 찾고, 없다면 스코프 체인을 따라 올라가며 찾는다.
  1. 함수 실행이 마무리되면 해당 컨텍스트는 사라진다.(클로저 제외)페이지가 종료되면 전역 컨텍스트가 사라진다.
4번에 함수 실행이 마무리되는 것은 함수의 모든 문이 종료된 후 실행 컨텍스트가 사라지는 것을 뜻한다.

12-2. 콜 스택(Call Stack)

콜 스택은 실행컨텍스트를 다루는 데에 아주 필수적인 요소이다. 함수가 호출되면 새로운 실행 컨텍스트가 생성되어 스택 맨 위에 추가(push)되고, 해당 함수의 작업이 완료될 때마다 해당 실행 컨텍스트는 제거(pop)된다. 이렇게 콜 스택을 사용하여 JavaScript 엔진은 프로그램에서 어느 부분이 현재 동작 중인지 파악할 수 있다.
실행컨텍스트의 개념을 설명할 때 “실행 컨텍스트는 스택 형태로 관리되며, 함수가 호출될 때마다 새로운 컨텍스트가 생성된다. “ 라고 설명을 했었다. 여기서 스택은 어떤 구조를 가지고 있는지 설명하려고 한다.

12-2-1. 스택 구조 파악하기

실행 컨텍스트들은 스택(Stack)이라는 자료 구조를 통해 관리된다. 스택은 “마지막에 들어간 것이 먼저 나오는” (Last In, First Out : LIFO)특징을 가진 자료 구조이다.
실행 컨텍스트의 생명주기를 간단하게 설명하면 다음과 같다.
  • 초기 단계: 전역 코드가 로드될 때 전역 실행 컨텍스트가 생성되고, 이것이 처음으로 스택에 푸시(push)된다.
  • 중간 단계: 함수 호출 시 해당 함수의 실행 컨텍스트가 생성되며, 이것이 현재 진행 중인 상위 작업을 일시 중단하고 새롭게 생성된 실행 컨텍스트를 처리하기 위해 매번 스택의 최상단으로 올라간다.
  • 마지막 단계: 함수 내부에서 모든 코드의 처리가 완료되면 해당 함수의 실행 컨텍스트가 가 종료되며(pop), 그 아래 있는(즉, 호출한 곳에 해당하는) 실행 컨텍스트로 돌아간다.
function c(){ console.log('C'); } function b(){ console.log('B'); c(); } function a(){ console.log('A'); b(); } a(); // output: A,B,C
위 코드의 스택 실행 형태를 그림으로 표현하면 다음과 같다.
a를 호출하면 console에 “A”가 찍히고 b를 호출한다. “B”가 console에 찍힌 뒤 c가 호출된다.
💡
여기서 a() b() c() 모두 Call stack에 담겨(push)있다. Call stack에서 사라지는 조건 즉 pop이 되는 조건은 참조하거나 같이 묶여있는 함수나 코드의 모든 문이 종료되었을 때 사라지므로 C의 console이 찍히기 전까지 c부터 b, a function은 전부 Call stack에 머물러 있다.
마지막으로 “C”가 console에 찍힌다. 그리고 c()가 종료되고 스택에서 pop 되어 사라진다. 이후 b() , a()도 순차적으로 pop 되어 사라진다.

12-2-2. 스택의 이해를 위한 재귀함수 예시

재귀함수란?
자기 자신을 호출하는 함수이다. 재귀함수를 이런 식으로 부른다면 어떻게 실행될까?
function recursiveFunc() { console.log("재귀함수 실행"); recursiveFunnc(); } recursiveFunnc();
위 코드에서 recursiveFunc함수가 실행될 때마다 자기 자신을 다시 호출하고 있다. 그런데 이 함수에는 재귀 호출을 멈추게 하는 종료 조건이 없다. 따라서 이 함수는 무한히 계속해서 자기 자신을 호출하게 된다. 함수가 호출될 때마다 해당 함수의 실행 컨텍스트가 스택에 쌓이게 되는데 스택의 크기는 한정적이다. 따라서 위 코드처럼 종료 조건 없이 계속해서 재귀 호출이 일어나면, 결국 스택의 공간이 부족해져서 더 이상 실행컨텍스트를 스택에 담을 수 없게 된다. 이를 stack overflow라고 한다. 이렇게 되면 "Maximum call stack size exceeded"와 같은 에러 메시지와 함께 프로그램은 중단되게 된다.
재귀함수를 사용할 때는 반드시 언젠가는 재귀호출이 멈추도록 하는 종료 조건을 설정해야 한다.

12-3. 실행 컨텍스트의 세부 사항 이해

실행 컨텍스트는 3가지가 있다.
  • Global execution context = Global Object(GO라고 부른다.)
    • this object
    • window object
  • Function execution context = Activation Object(AO라고 부른다.)
  • Eval execution context = 보안상 위험하므로 쓸 일이 없다.

12-3-1. 실행 컨텍스트의 생성시점

실행컨텍스트의 생성 시점은
  • Global Execution Context: JavaScript 엔진이 스크립트를 처음 마주할 때 전역 컨텍스트(Global Context)를 생성하고 콜 스택(실행 컨텍스트 스택) 에 push한다.
  • Function Execution Context: 엔진이 스크립트를 읽으면서 함수 호출을 발견할 때마다, 함수의 실행 컨텍스트를 스택에 push한다.
    • 함수 실행 컨텍스트는 함수가 실행될 때 만들어진다.
  • Eval Execution Context : eval()함수를 실행하면 실행 컨텍스트가 생성된다.
    • eval()은 사용할 일이 없기 때문에 3번은 무시해도 된다.
    • 자바스크립트 공식 문서 에서도 eval()을 절대 사용하지 말 것!"이라고 적혀 있을 정도로 보안상 위험하다고 함.

12-3-2. 실행 컨텍스트의 단계

실행 컨텍스트는 생성 단계와 실행 단계 두 단계로 나뉘어 있다.
Createion Phase(생성 단계)
  • GO, AO, this 형성된다.
💡
Global Object(GO): 자바스크립트에서 전역 객체(Global Object)는 모든 객체의 부모 역할을 하는 특별한 객체입니다. 웹 브라우저 환경에서 전역 객체는 보통 window입니다. 이 window객체 내부에는 다양한 내장 함수와 속성들이 포함되어 있습니다 또한 BOM, DOM도 포함이 된다. Activation Object(AO): 실행 컨텍스트가 생성될 때마다, 해당 컨텍스트 안에서 사용되는 변수와 함수 등을 저장하기 위해 활성화 객체(Activation Object)가 생성된다. 이 AO에는 변수, 함수 선언, 매개변수(arguments)등이 포함된다.
  • ScopeChain형성이 된다. 이 때문에 호이스팅이 가능한 것이다.
    • GO,AO가 형성될 때 먼저 코드를 훑으면서 서로서로 ScopeChain링크를 걸어서 참조가 가능.
    • ScopechainScopechain
      Scopechain
  • 값이 들어가 있지 않는 초깃값(var은 선언과 초기화 , let/const는 선언만)
 
실행단계(Execution Phase)
  • GO, AO, this에 값이 할당된다.
  • this는 함수 호출 패턴 또는 lexical scope에 따라 값이 정해진다.

12-4. 실행 컨테스트 속의 정보

우선 실행 컨텍스트 내부엔 variable environmentlexical environmentthis binding 가 있다. 하나씩 살펴보도록 하자.

12-4-1. VariableEnvironment

VariableEnvironment란 현재 컨텍스트 내부의 식별자 정보 EnvironmentRecord, 외부 환경 정보 OuterEnvironmentReference가 포함되어 있다. VariableEnvironment에 먼저 정보를 담고, 그대로 LexicalEnvironment에 복사해 사용한다.
💡
EnvironmentRecord란?
현재 컨텍스트와 관련된 식별자와 식별자에 바인딩 된 값이 기록되는 공간이다. 더불어 실행 컨텍스트 내부 전체를 처음부터 끝까지 확인하며 순서대로 수집한다.
💡
OuterEnvironmentReference란? 해당 함수가 선언된 위치의 LexicalEnvironment를 참조하고, 변수에 접근을 한다면 해당 LexicalEnvironment에서 발견되면 사용한다. 찾지 못할 경우 다시 outerEnvironmentReference를 참조하여 탐색하는 과정을 반복한다. 이러한 과정을 scope chain이라고 하며 outerEnvironmentReference는 스코프체인을 가능하게 하는 역할이다.

12-4-2. LexicalEnvironment

LexicalEnvironment는 초기엔 VariableEnvironment와 같지만, 변경 사항이 실시간으로 적용된다. 즉, VariableEnvironment의 초기 상태를 기억하고 있으며, LexicalEnvironment의 최신 상태를 저장 하고 있다.
💡
호이스팅(Hoisting) 자바스크립트 엔진이 실행 컨텍스트를 구성할 때 EnvironmentRecord에 식별자의 정보를 수집하는데, 이러한 과정을 통해 엔진은 함수를 실행하기도 전에 해당 컨텍스트 내부의 변수명들을 이미 알고 있다. 이렇기에 식별자들을 코드의 최상단으로 끌어올렸다는 호이스팅이라는 개념이 생겼다. 코드가 자동으로 끌어올려진 것이 아닌, 실행 컨텍스트 관점에선 코드를 동작했을 때 이미 식별자들의 정보를 알고 있으니 식별자 정보를 수집하는 과정을 이해하기 쉬운 방법으로 나타낸 추상화한 가상 개념이다. 아래 코드에서 ReferrenceError가 출력되는 것이 아닌 undefined가 출력되는 이유이다.
console.log(name); // output: undefined(호이스팅 현상) var name = 'Kim';

12-4-3. ThisBinding

ThisBinding은 실행 컨텍스트에서의 this로 지정된 객체의 정보를 갖고 있는 것이다. this는 실행 컨텍스트에 따라 다르게 바인딩이 된다. 이 부분은 this에 대한 이해도가 필요하다. 자바스크립트를 공부할 때 this 키워드가 가장 많은 혼동을 준다. 이해를 돕기 위해 this 키워드에 관해 상세히 설명할 것이다.
먼저 this는 컨텍스트를 가르킨다. 메서드(method)에서 this를 사용 시 해당 메서드 가 담겨있는 인스턴스(instance) 또는 오브젝트(object)를 가르키며, 함수 표현식에서 사용시 this를 바인딩하지 않는 이상 전역 객체를 가리킨다. 또한 전역 공간에서 this는 함수 표현식과 같이 전역 객체를 가리킨다. 설명 하자면 각 실행 문맥에서 this를 바인딩하는 데에는 일정한 규칙이 존재한다. 함수가 호출되는 상황에 따른 규칙들이다. 각 규칙에 따라서 우선순위가 존재하는데 이를 설명과 함께 알아보자.
  1. 기본 바인딩
    1. // Browser환경에서의 실행 function showThis(){ console.log(this); } function showThisInStrictMode(){ 'use strict' console.log(this); } showThis(); // output : window showThisInStrictMode(); // output : undefined
      • Browser 환경에서의 경우, 함수를 위 코드와 같이 단독 실행하게 되면 this는 기본적으로 전역객체에 바인딩된다. 유의할 점은 ‘use strict’키워드를 통해서 엄격모드를 사용하게 되면 전역객체가 기본 바인딩 대상에서 제외가 되어 this가 바인딩될 객체가 존재 하지 않기 때문에 ‘undefined’값을 가진다.
      // Node환경에서의 실행 function showThis(){ console.log(this); } showThis(); // output : object [global] console.log(this); // {} console.log(this === module.exports); // true
      • Node 환경에서의 경우, 마찬가지로 this가 node의 전역객체인 global객체에 바인딩이 되는데, 함수코드 안에서가 아닌 전역 코드상에서 this를 콘솔에 출력해 보면 빈 객체가 나오게 된다. 이 빈 객체는 사실 모듈 객체에 있는 exports 객체와 동일한 객체이다. 이를 통해서 노드JS 환경에서 this는 두 가지 방향으로 바인딩이 된다고 알 수가 있다.
  1. 암시적 바인딩
    1. 자바스크립트 함수는 단독으로 호출될 수 있을 뿐만 아니라 객체의 메서드로도 호출이 된다.
      notion imagenotion image
      이 경우에는 this가 .(마침표 연산자) 바로 앞에 있는 객체에 바인딩 된다. 그리고 이렇게 바인딩 되는 방식을 암시적 바인딩이라고 부른다. 암시적 바인딩이 되는 경우에 함수를 사용할 때는 조심해야 할 부분이 있다.
      const obj = { name : "Kim", getName() { return this.name; }, }; function showReturnValue(callback) { console.log(callback()); } showReturnValue(obj.getName); // output : undefined
      이 코드는 obj라는 변수 안에 객체로 두 개의 프로퍼티가 있고, showReturnValue라는 함수가 callback이란 값을 인자로 받고 callback에 대한 반환 값을 콘솔에 출력을 해준다. 그리고 showReturnValue에다가 obj.getName를 인수로 넘겨 함수 호출을 하는 코드이다. 앞서 암시적 바인딩의 설명에 의하면 getName함수 마침표 연산자 앞에 있는 객체가 obj이기 때문에, 그 안에서의 this가 obj에 바인딩 되어 콘솔에 “Kim”이 출력이 되어야 한다고 생각이 될 수 있다. 하지만 실행결과는 undefined이다. 이러한 결과물에 이유는 점 연산자나 대괄호 연산을 통해서 객체의 프로퍼티에 우리가 접근을 하면 참조 타입(Reference Type)이라고 하는 특별한 값을 반환 해 준다. 참조 타입은 자바스크립트 명세서에서만 사용되는 타입인데, 프로퍼티 뿐만 아니라 프로퍼티를 가지고 있는 객체와 ‘strict mode’에 대한 여부를 같이 가지고 있는 하나의 타입이다.
      notion imagenotion image
      예를 들어서 ‘strict mode’에서 obj.getName에 접근하게 되면 위 그림의 순서대로 obj, getName, true 와 같은 참조 타입을 얻게 된다. 이러한 참조 타입에 바로 괄호를 붙여서 함수를 호출하게 되면 함수 안에 있는 this가 참조 타입에 있는 객체를 찾아서 바인딩 된다. 하지만 점 연산자나 대괄호 연산을 제외한 다른 연산들은 참조 타입이 아닌 해당 프로퍼티의 값만 전달하게 된다. 따라서 아무리 점 연산자를 통해 참조 타입을 얻어냈다고 하더라도 다른 변수에 할당을 하는 순간 프로퍼티의 값 혹은 프로퍼티가 참조하고 있는 참조 값만 남게 되는 것이다. 그래서 점 연산자를 통해서 얻은 값은 바로 함수로 호출하지 않고서는 암시적 바인딩을 기대할 수가 없다. 마찬가지로 아까 코드에서 인수로 전달된 callback의 참조도 객체에 대한 어떠한 정보도 포함을 하지 않기 때문에 함수로 단독 호출한 것과 같이 동작을 하게 되는 것이다.
  1. 명시적 바인딩
    1. 암시적 바인딩의 설명처럼 다른 변수에 할당하는 순간 바인딩 된 this를 너무 쉽게 잃어버리는 문제가 있는데 이를 해결할 수 있는 방법이 명시적 바인딩이다. 이를 통해 this가 소실되는 문제를 해결할 수 있다. 함수 객체는 call, apply, bind 등의 메서드를 통해서 명시적으로 this를 바인딩 할 수 있는 방법을 제공한다. call과 apply는 하는 역할은 인수를 전달하는 방식 말고는 거의 유사하다.
      call(context,arg1,arg2,...) apply(context,args) func.bind(context,arg1,arg2,...) // context <=> this를 바인딩할 객체
      call: 인수를 하나하나 전달 하는 방식 apply: 인수들을 배열 형태나 유사 배열 형태로 전달 하는 방식 bind: this가 참조하는 객체를 고정해 주고, 이후에는 인수로 사용될 값들을 넣어준다. 이 메서드가 반환하는 특수한 객체가 있는데 그 객체는 마치 항상 this가 어떤 특성 객체에 바인딩 되어 있는 함수처럼 행동을 한다.
      이렇게 항상 같은 객체에 바인딩 되도록 강제하는 방법을 하드 바인딩이라고 도 부른다.
  1. new 바인딩
    1. new 연산자로 함수를 호출하게 되면 생성자 함수로서의 역할을 수행할 수 있게 된다. 이를 순서대로 간략하게 설명하면,
    2. 새로운 객체를 그 안에서 하나 만든다.
    3. 함수의 코드를 실행한다.
    4. 새로 생성한 객체를 반환해 준다.
    5. 이 과정에서 this는 새로 생성한 객체에 바인딩 된다.
      { obj = {} // 새로운 객체생성 this = obj // bind this.name = "Kim" // obj : {name : "Kim} return this }
      이러한 과정을 통해 프로퍼티가 정해진 객체를 반환하는 생성자의 역할을 수행할 수 있다. 이렇게 new 연산자로 함수를 호출할 때 this가 바인딩 되는 규칙을 new binding이라고 한다.
 
설명한 네 가지 케이스 중 하나만 해당되는 것이 아니라 중복으로 해당되는 경우가 있다.
이 경우에는 우선순위를 통해 결정하게 된다.
위 그림 순서대로 new연산자 바인딩이 가장 우선순위가 높다. 그리고 어떠한 규칙에서도 해당되지 않을 때는 전역 객체 또는 undefined 값을 가지게 된다.
마지막으로 ES6에 추가된 화살표 함수(arrow function)에 this가 바인딩이 되는데, 다른 함수들과는 바인딩하는 방법이 좀 다르다. JavaScript에서는 어떤 식별자(변수)를 찾을 때 현재 환경에서 그 변수가 없으면 바로 상위 환경을 검색한다. 그렇게 점점 상위 환경으로 타고 타고 올라가다가 변수를 찾거나 가장 상위 환경에 도달하면 그만두게 되는 것이다. 화살표 함수에서의 this 바인딩 방식도 이와 유사하다. 화살표 함수에는 this라는 변수 자체가 존재하지 않기 때문에 그 상위 환경에서의 this를 참조하게 된다. 더 정확히는, function으로 선언한 함수가 메서드로 호출 되냐 함수 자체로 호출 되냐에 따라 동적으로 this가 바인딩 되는 반면, 화살표 함수는 선언될 시점에서의 상위 스코프가 this로 바인딩 된다.
const obj = { value: 'Hello World', showValue: function() { setTimeout(() => { console.log(this.value); }, 1000); } }; obj.showValue(); // "Hello World"
위 코드에서 showValue() 메서드 내부에 정의된 화살표 함수는 자신만의 this를 생성하지 않는다. 대신, 상위 스코프(위 코드에서는 showValue() 메서드)의 this를 참조한다. 따라서 setTimeout의 콜백 내부에서도 'Hello World'가 출력 되는 것이다.
이로써 this에 대한 설명은 마치고, “실행컨텍스트에서의 this로 지정된 객체의 정보를 갖고 있는 것이 ThisBinding이다.” 라는 설명을 하기 위함 이였다.

12-5. 실행 컨텍스트 예시

앞서 실행 컨텍스트에 대한 대충의 흐름은 이해가 갔을 것이다. 코드와 해석을 위한 그림을 보면서 같이 이해 해보자.
var name = 'Kim'; function second (){ console.log('HI'); } function first (){ second(); } first();
아래는 위 코드를 실행 시켰을 때 콜 스택이 동작하는 방식을 그림으로 표현한 것이다.
그림과 함께 번호 순서대로 설명 하겠다.
  1. 자바스크립트 코드를 실행하는 순간 “1”처럼 전역 컨텍스트가 콜스택에 담긴다.
  • 브라우저의 경우 window, node환경일 경우 global같은 객체를 사용할 수 있는 이유이다.
  1. 전역 컨텍스트와 관련된 코드를 진행 중 a함수를 실행하였기에 a함수의 환경 정보들을 수집하여 a실행 컨텍스트를 생성, 콜스택에 담는다. 콜스택 최상단에 a 실행 컨텍스트가 있기에 기존의 전역 컨텍스트와 관련된 코드의 실행을 일시적으로 중단한 후 a 실행 컨텍스트의 코드를 실행한다.
  1. a함수 내부에서 b함수를 실행하였기에 b함수의 환경 정보들을 수집, 실행 컨텍스트를 생성, 콜스택에 담는다. 이전과 똑같이 콜스택 최상단에 b실행 컨텍스트가 있기에 기존 a실행 컨텍스트와 관련된 코드의 실행을 일시적 중단한다.
  1. b함수가 종료된 후 b실행 컨텍스트가 콜스택에서 제거된다. 제거 후 콜스택 최상단에는 a실행 컨텍스트가 있기에 이전에 중단된 지점부터 코드 진행이 재개된다.
  1. a함수 또한 종료된 후 실행 컨텍스트가 콜스택에서 제거됩니다.
이후엔 전역 공간에 실행할 코드가 남아있지 않다면 콜스택에서 전역 컨텍스트 또한 제거되며 콜스택에 아무 것도 남지 않은 상태로 종료된다.