자바스크립트는 싱글 스레드 인터프리터 언어로, 각 브라우저는 자체 자바스크립트 엔진을 사용하여 코드를 스캔하고 해석합니다. 이 과정에서 중요한 역할을 하는 것이 바로 실행 컨텍스트(Execution Context) 입니다. 실행 컨텍스트는 코드의 변환과 실행을 담당하는 환경으로, 실행할 코드에 필요한 모든 정보를 담은 객체라고 할 수 있습니다.

🔎 Execution Context 실행 컨텍스트를 알아야하는 이유?

  • 자바스크립트를 사용하는 개발자라면, 실행 컨텍스트라는 개념에 대해 잘 알고 있는 것은 중요합니다.이 개념을 이해하는 것은 단순히 이론적인 지식을 넘어, 실제 개발 과정에서 큰 도움을 주기 때문입니다.

  • 코드를 디버깅하는 상황을 생각해 보면, 왜 이 변수가 이 값이 되었는지, 왜 이 함수가 바로 여기서 호출되었는지와 같은 궁금증들이 생길 때가 많습니다. 실행 컨텍스트를 이해하면 이런 의문들을 훨씬 쉽게 해결할 수 있게 됩니다. 변수의 스코프(Scope), 호이스팅(Hoisting), 함수 호출 방식 등을 이해하면, 코드의 흐름을 좀 더 명확히 파악하는데 큰 도움이 됩니다.

  • 또한, 코드를 작성할 때도 이점이 있습니다. 어떻게 코드가 실행되고, 메모리에 어떻게 할당되는지 아는 것은 효율적이고 깨끗한 코드를 작성하는 데 도움이 됩니다. 이는 웹 애플리케이션의 전반적인 성능 향상으로 이어집니다.

  • 마지막으로 다른 프로그래밍 기술을 익히는 데도 실행 컨텍스트의 이해는 필수적입니다. 클로저(Closure), 콜백(CallBack), 프로미스(Promise)자바스크립트의 고급 개념들이 모두 실행 컨텍스트와 밀접하게 연결되어 있습니다. 결론적으로 해당 개념들을 제대로 파악하고 싶다면, 실행 컨텍스트를 잘 이해하는 것은 필수적입니다.

😅 나의 작은 경험?

  • 실제 개발을 하다 보면, 실행 컨텍스트를 깊이 이해해야 하는 상황이 자주 발생하곤 합니다. 제 경험을 예로 들어보면, 여러 비동기 작업을 관리해야 하는 상황에서 자바스크립트의 이벤트 루프와 실행 컨텍스트의 관계를 제대로 이해하지 못한 상태로 개발을 하게되었습니다.

  • 그 결과, **콜백(CallBack)**과 **프로미스(Promise)**를 사용하면서 예상치 못한 타이밍에 코드가 실행되고, 상태 관리 문제와 데이터 불일치 문제가 생기곤 했습니다. 당시엔 분명, 제대로 변수에 값을 할당했는데, 왜 넣은 데이터가 출력되지 않는지 이해할 수 없었고, 애꿎은 데이터만 console에 열심히 출력해 보고 했던 기억이 있습니다.

  • 결국 이 문제를 해결하기 위해서는 자바스크립트 엔진이 어떻게 비동기 코드의 실행 컨텍스트를 처리하는지, 이벤트 루프가 어떻게 동작하는지 이해해야 했습니다. 이 모든 개념의 시작엔 실행 컨텍스트가 있었고, 그래서 도대체 실행컨텍스트란 무엇이냐는 생각으로 이 개념에 대해서 자세히 공부해 보게 되었습니다.


🌟Execution Context 실행 컨텍스트란?

  • 사용자가 웹페이지에 처음 접근할 때, 사용 중인 브라우저의 자바스크립트 엔진이 자바스크립트 파일을 스캔합니다.

  • 스캔이 완료되면, 스크립트의 모든 코드를 변환하고 실행하는 과정을 관리하는 '실행 컨텍스트(execution context)'라는 환경이 생성됩니다.

  • 환경은 실행할 코드에 전달할 정보들을 담고 있는 객체로 볼 수 있습니다.

  • 여기에는 스코프(Scope), 호이스팅(Hoisting), 클로저(Closure), this, function 등의 동작 원리가 포함되어 있으며, 이러한 원리들은 자바스크립트의 핵심 원리로 간주됩니다.

  • 이러한 방법으로, 자바스크립트는 해당 스크립트 코드에 있는 실행 컨텍스트에 관한 정보들을 수집합니다.

  • 모아진 실행 컨텍스트를 콜스택에 쌓아올린 후 실행하여 코드의 실행환경과 순서를 보장하게 됩니다.

  • 또한, 각각의 실행 컨텍스트는 활성화되는 시점에 해당 컨텍스트 내부에 선언된 변수를 위로 끌어 올리고(hosting), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행하게 됩니다.


❗ 예시

var a = "a";

function first() {
  console.log("This is first");
}

function second() {
  first();
}

second();

해당 코드와 그림은 처음 자바스크립트가 실행되고 콜스택에 어떤식으로 컨텍스트들이 담기고, 작동하게 되는지에 대한 예시입니다.

전역 컨텍스트는 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해할 수 있습니다.
❗ 참고로 전역컨텍스트의 경우, 브라우저 환경에서는 window 객체로, Node.js 환경에서는 global 객체로 표현됩니다.

  1. 전역 컨텍스트가 생성되어 콜 스택에 푸시됩니다.
  2. second() 함수를 호출하면, 해당 함수의 새 실행 컨텍스트가 생성되어 콜 스택의 최상단에 푸시됩니다.
  3. second 내부에서 first()를 호출하면, first의 실행 컨텍스트도 콜 스택의 최상단에 푸시됩니다.
  4. first 함수가 종료되면, 해당 실행 컨텍스트는 콜 스택에서 팝되며 삭제됩니다.
  5. 이후 second의 실행 컨텍스트도 스택에서 팝되며, 마지막으로 전역 컨텍스트가 남습니다.
  6. 최종적으로 전역 컨텍스트도 스택에서 팝되며, 프로그램의 실행이 종료됩니다.

실행 컨텍스트(Execution Context) 의 구성

다음은 실행컨텍스트가 실제로 어떻게 구성되어있는지에 대한 구조도입니다.
실행 컨텍스트는 크게 3가지 부분으로 나뉘어져있으며, 각각의 부분들은 다른 역활들을 수행하게 됩니다.

🌍 Variable Environment

  • Variable Environment는 현재 실행 컨텍스트 내의 변수와 함수, 그리고 외부 환경에 대한 정보를 포함합니다. 이 환경은 environmentRecordouterEnvironmentReference로 구성됩니다.
    • environmentRecord는 실행 컨텍스트 내에서 선언된 변수와 함수 선언들의 실제 값을 저장합니다.
    • outerEnvironmentReference는 외부 환경에 대한 참조를 유지합니다. 이 참조는 현재 컨텍스트가 위치한 코드의 외부 스코프, 즉 부모 스코프에 대한 정보를 가집니다.
  • 실행 컨텍스트가 최초에 생성될 때, Variable Environment에 이 정보들이 저장되고, 이후 Lexical Environment를 형성하기 위해 복사됩니다.
  • Variable Environment는 초기화 시점의 상태를 스냅샷으로 유지하며, 후에는 참조만 제공합니다. 이는 실행 컨텍스트의 초기 상태를 추적할 때 유용합니다.

🌍 Lexical Environment

  • Variable Environment와 마찬가지로, Lexical EnvironmentenvironmentRecordouterEnvironmentReference로 구성됩니다.
    • Lexical Environment의 environmentRecord는 실행 중에 코드 내에서 발생하는 변화를 실시간으로 반영합니다. 이는 블록 내의 변수 할당이나 함수 표현식과 같은 동적인 활동을 포함합니다.
    • outerEnvironmentReference는 Lexical Environment의 외부 스코프에 대한 참조를 제공합니다. 이는 현재 컨텍스트가 어떤 외부 스코프와 연결되어 있는지를 나타냅니다.
  • Lexical Environment는 코드 실행 중에 발생하는 동적인 활동을 반영함으로써 Variable Environment와 차별화됩니다. 이를 통해 실행 컨텍스트의 실시간 상태를 보다 정확하게 반영하게 됩니다.

🌍 Environment 의 구성

  • 📦 environmentRecord

    • 함수 내의 코드가 실행되기 전에, 현재 컨텍스트에 관련된 모든 식별자 정보 (매개변수의 이름, 함수 선언, 변수명 등)가 여기에 저장됩니다.
    • 이 과정을 통해 JavaScript 엔진은 코드 실행 전에 해당 환경의 식별자들을 인지하게 되며, 이것이 바로 호이스팅입니다. 호이스팅은 코드에서 선언들을 먼저 처리하고 나중에 할당을 수행하는 JavaScript의 동작을 추상화한 개념입니다. 함수 선언문은 전체가 호이스팅되지만, 함수 표현식은 이름만 호이스팅되고 함수 본문은 실행 흐름이 해당 위치에 도달했을 때 처리됩니다.
  • 📑 outerEnvironmentReference

    • 이는 현재 실행 컨텍스트의 상위 스코프를 참조합니다.
    • 다시 말해, 현재 environmentRecord 외부에 있는 LexicalEnvironment를 참고하는 것으로, 이를 통해 해당 실행 컨텍스트를 생성한 함수의 외부 환경에 접근할 수 있습니다.
    • 코드에서 변수를 찾을 때 현재 컨텍스트의 LexicalEnvironment를 먼저 검색하고, 그곳에서 찾지 못하면 outerEnvironmentReference를 통해 상위 스코프를 검색합니다. 이 검색은 전역 컨텍스트의 LexicalEnvironment에 도달할 때까지 계속되며, 결국 해당 변수를 찾지 못하면 undefined를 반환합니다.

❗예제

const greeting = "Hello World";

const petInfo = () => {
  const cat = {
    age: 5,
    breed: "Siamese",
  };
  const dog = {
    age: 8,
    breed: "Labrador Retriever",
  };
  console.log(greeting); // 'Hello World'
  console.log(cat); // { age: 5, breed: 'Siamese' }
  console.log(dog); // { age: 8, breed: 'Labrador Retriever' }
};

petInfo();

console.log(cat); // ReferenceError: cat is not defined
console.log(dog); // ReferenceError: dog is not defined
  • 이 코드에서 petInfo 함수 안에서 greeting, cat, dog 변수를 모두 사용할 수 있습니다.
  • greeting은 함수 바깥에서 선언되었기 때문에, 함수 내부에서도 접근할 수 있습니다. 이는 outerEnvironmentReference 덕분입니다. outerEnvironmentReference는 함수가 위치한 스코프의 바깥 환경, 즉 여기서는 전역 환경을 가리킵니다.
  • catdogpetInfo 함수 내부에서 선언되었으므로, 함수 외부에서는 접근할 수 없습니다. 이 변수들은 petInfoLexicalEnvironment에 존재하며, 이 함수가 끝나면 더 이상 접근할 수 없게 됩니다.
  • petInfo 함수 내부에서는 자신의 LexicalEnvironment에서 catdog를 찾고, outerEnvironmentReference를 통해 전역 스코프에서 greeting을 찾을 수 있습니다.
  • outerEnvironmentReference는 함수가 선언된 위치의 스코프 체인을 따라 올라가면서 필요한 변수를 찾습니다. 이 예에서는 전역 스코프까지 올라가 greeting을 찾아냅니다.
  • 하지만 함수 바깥에서 cat이나 dog를 찾으려고 하면, 이 변수들은 petInfo 함수의 LexicalEnvironment에 속해 있고, 바깥 스코프인 전역 스코프의 LexicalEnvironment에는 존재하지 않기 때문에 ReferenceError를 반환합니다.

🔗 this binding

☝️ 바인딩이란? 프로그래밍에서 바인딩은 식별자(예: 변수, 함수 이름)를 그들이 대표하는 값과 연결하는 과정을 의미합니다. 예를 들어, 변수 선언은 변수 이름을 메모리 상의 주소와 연결합니다. this 바인딩은 특별한 경우로, this라는 식별자와 그것이 가리키는 객체를 연결합니다.

☝️ Lexical Scope와의 차이: 함수의 Lexical Scope는 그 함수가 정의된 위치에 따라 결정되는 반면, this 바인딩은 함수가 어떻게 호출되는지에 따라 달라집니다. 즉, this 바인딩은 함수의 선언 위치와 상관없이, 오로지 어디서 어떻게 함수를 호출하느냐에 따라 결정됩니다.

함수를 호출하는 방법과 this 바인딩

  1. 일반 함수 호출 (기본 바인딩): 함수가 일반적인 방식으로 호출될 때, this는 전역 객체(브라우저에서는 window, Node.js에서는 global)를 가리킵니다. 단, 'strict mode'에서는 thisundefined가 됩니다.

  2. 메서드 호출 (암시적 바인딩): 객체의 메서드로서 함수가 호출되면, this는 그 메서드를 호출한 객체를 가리킵니다. 예를 들어, obj.method()에서 thisobj를 가리킵니다.

  3. Function.prototype.apply/call/bind 메서드에 의한 간접 호출 (명시적 바인딩): 이러한 메서드들을 사용하면, 개발자가 명시적으로 this를 바인딩할 객체를 지정할 수 있습니다. 이 경우, this는 지정된 객체를 가리킵니다.

  4. 생성자 함수 호출 (new 바인딩): new 키워드를 사용하여 함수를 생성자로 호출하면, this는 새로 생성된 객체를 가리킵니다.

    • 예외 사항: 'strict mode'에서는 this 바인딩이 다르게 작동합니다. 일반 함수에서 thisundefined로 설정됩니다.
    • 실용적인 예시: 코드 예시를 통해 각 호출 방식에서 this가 어떻게 바인딩되는지 실제로 보여줄 수 있습니다. 이를 통해 this 바인딩의 개념을 더 명확히 이해할 수 있습니다.

❗예제

// 1. 일반 함수 호출
// 'this'는 기본적으로 전역 객체를 가리킴 (브라우저에서는 window, Node.js에서는 global)
// 'strict mode'에서는 'this'가 undefined로 설정됨
const first = function () {
  console.log(this);
};
first(); // window 또는 undefined ('strict mode'에 따라 달라짐)

// 2. 메서드 호출
// 'this'는 메서드를 호출한 객체를 가리킴
const obj = { first };
obj.first(); // { first: ƒ first() }

// 3. call을 사용한 호출
// 'this'는 call의 첫 번째 인자로 명시적으로 지정된 객체를 가리킴
first.call({ a: "kim" }); // { a: 'kim' }

// 4. 생성자 함수 호출
// 'this'는 새로 생성된 인스턴스를 가리킴
new first(); // first {}

// 5. 화살표 함수
// 화살표 함수는 'this'를 자체적으로 바인딩하지 않고, 정의된 상위 스코프의 'this'를 사용함
const arrowFunc = () => {
  console.log(this);
};
arrowFunc(); // 상위 스코프의 'this' (일반적으로 전역 객체 또는 'strict mode'에서는 undefined)

❗실제 출력 화면

  • 화살표 함수는 자신의 this를 생성하지 않고, 정의된 상위 스코프의 this를 사용합니다. 이는 화살표 함수가 어휘적으로 주변 스코프(Lexical Scope)를 상속받는다는 것을 의미합니다.
  • 'this' 바인딩의 우선순위:
    1. new 바인딩: new 키워드로 호출된 생성자 함수에서 this는 새로 생성된 인스턴스를 가리킵니다.
    2. 명시적 바인딩: call, apply, bind를 사용할 때 this는 명시적으로 지정된 객체를 가리킵니다.
    3. 암시적 바인딩: 객체의 메서드로 함수가 호출되면 this는 그 객체를 가리킵니다.
    4. 기본 바인딩: 'strict mode'에서는 thisundefined가 되며, 'non-strict mode'에서는 전역 객체를 가리킵니다.

KDK

참조