자바스크립트의 실행 컨텍스트 이해하기 (+ 스코프 체인, 호이스팅)

드뮴·2024년 12월 30일
6

🧀 자바스크립트

목록 보기
1/1
post-thumbnail

함수는 선언 전 사용이 가능한데 클래스는 선언 전 사용하는게 불가능했다. 호이스팅과 관련된 개념이었는데 호이스팅이 정확히 무엇인지 잘 모른다는 것을 깨닫고 호이스팅을 공부해보았다. 또한 호이스팅을 이해하기 위해서 자바스크립트가 코드를 실행하는 과정을 이해할 필요가 있다고 느껴 처음부터 공부해보았다.


자바스크립트 엔진은 코드를 실행을 위해 실행 컨텍스트라는 환경을 생성한다. 이때 2가지 일이 일어난다.
첫번째로는 호이스팅을 통해 코드에서 선언된 변수와 함수를 찾아 미리 처리한다. 두번째로는 스코프 체인이 형성되어 실행 컨텍스트의 렉시컬 환경을 서로 연결해 변수를 찾을 수 있게 만든다.
2가지 설정이 완료되면 코드가 한 줄씩 실행되고, 변수에 접근할 때마다 스코프 체인을 따라 적절한 값을 찾아 사용한다.


자바스크립트 엔진이 코드를 실행하는 과정

자바스크립트 엔진이 코드를 실행할 때는 크게 두 단계를 거친다. 이 두 단계는 생성 단계와 실행 단계가 있는데 각 단계는 다음과 같다.

1. 생성 단계(Creation Phase)

실행 컨텍스트가 생성된다.

  • environmentRecord 생성
  • 외부 환경 참조(outerEnvironmentReference) 설정
  • this 바인딩

실제 구조

ExecutionContext = {
  // 변수 환경
  VariableEnvironment: {
    environmentRecord: {
      // var로 선언된 변수
      x: undefined,
      a: undefined
    },
    outer: <outer scope reference>
  },

  // 렉시컬 환경
  LexicalEnvironment: {
    environmentRecord: {
      // let, const로 선언된 변수
      y: <uninitialized>,
      b: <uninitialized>
    },
    outer: <outer scope reference>
  },

  // ThisBinding
  ThisBinding: <this value>
}

변수 환경

  • environmentRecord: 변수 저장소로 초기 상태를 스냅샷처럼 유지하고, var 변수만 관리
  • outer(outerEnvironmentReference): 상위 스코프의 환경을 참조하는 포인터로, 함수 단위의 스코프 체인을 형성

렉시컬 환경

  • environmentRecord: 변수 저장소로 실시간으로 변경 사항을 반영하고, let, const 변수 관리
  • outer(outerEnvironmentReference): 상위 스코프의 환경을 참조하는 포인터로, 블록 단위의 스코프 체인을 형성

2. 실행 단계(Execution Phase)

코드가 한 줄씩 실행되는 단계


실행 컨텍스트란?


실행 가능한 코드를 형상화하고 구분하는 추상적인 개념으로, 실행 가능한 코드가 실행되기 위해 필요한 환경을 의미한다. 즉, 코드를 실행하기 위해 모든 정보를 가지고 있는 환경으로 생각하면 된다.

실행 컨텍스트가 활성화되는 시점에서 다음과 같은 일이 생긴다.

  • 호이스팅이 발생해서 선언된 변수를 위로 끌어올리는 것처럼 동작한다.
  • 외부 환경 정보를 구성한다.
  • this 값을 설정한다.

실행 컨텍스트의 종류

  1. 전역 실행 컨텍스트
    • 코드가 실행되면 가장 먼저 생성
    • 전역 변수와 함수가 여기에 정의
    • window 객체(브라우저의 경우)가 this가 됨
  2. 함수 실행 컨텍스트
    • 함수가 호출될 때마다 생성
    • 함수 내부의 변수와 arguments 객체를 포함
    • 함수의 매개변수와 지역 변수가 여기서 관리됨
  3. eval 실행 컨텍스트
    • eval 함수 실행
  4. 블록 실행 컨텍스트
    • ES6부터 도입된 let, const로 인해 생성
    • 블록 스코프를 가진 변수를 관리

코드를 실행하면 콜 스택은 어떻게 변화할까?


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

  • 프로그램을 실행하면 스택에는 전역 컨텍스트가 스택에 저장된다.
  • outer 함수를 실행하면, outer 함수가 스택에 쌓이고 inner를 실행하면 그 위에 inner가 쌓인다.
  • inner 함수가 반환되면 스택에서 제거되고, outer도 반환되면 스택에서 제거된다.
  • 최종적으로 전역 컨텍스트가 남게되고 프로그램이 종료되면, 전역 컨텍스트도 제거된다.

위 코드에서 console.log로 출력되는 값은?
inner 함수에서 console.log(a)를 출력하면 undefined가 출력된다. var a = 1로 바깥에서 선언해두었는데 왜 undefined로 출력되는지 이해하기 위해서는 변수의 스코프를 이해해야한다.
inner 함수 내에서는 var a = 3이라는 선언을 해주고 있다. 전역 스코프에서는 var a = 1이 실행되어 전역 변수 a가 1로 초기화된다. 그런데 inner 함수가 실행되면 자바스크립트 엔진은 var a라는 선언을 발견하게 되고, 이때 호이스팅으로 인해 a를 undefined로 초기화하기 때문에 console.log(a)를 하면 undefined가 출력된다.

function inner() {
  // 호이스팅으로 인해 아래와 같이 재구성
  var a;
  console.log(a); // undefined
  a = 3;
  console.log(a); // 3
}
  • 위와 같이 재구성되어 a는 undefined가 출력된다.
  • 그렇다면 만약 inner 함수 내부에 a를 선언해주는 코드를 삭제하게 되면, 이때는 전역 변수 a를 참조하게 되어 전역 변수 a 값이 출력된다.

개발자 도구에서 코드 실행해보기

개발자 도구를 통해 콜 스택 부분과 변수 a를 관찰하면 다음과 같다.

  • 함수가 호출되면 콜 스택에 추가되고 반환되면 콜 스택에서 제거된다.
  • 변수 a는 inner 함수에서 var a = 3이 있으므로 선언과 동시에 초기화가 되어 undefined로 존재한다. a = 3이 실행되고 나면 마지막 console.log에서는 a가 3이라고 출력된다.

실행 컨텍스트의 구성 요소

  1. 변수 환경(Variable Environment)
    • environmentRecord: 현재 컨텍스트 내의 식별자들에 대한 정보
    • outerEnvironmentReference: 외부 환경 정보
    • 현재 스코프의 모든 변수 선언을 저장
  2. 렉시컬 환경(Lexical Environment)
    • environmentRecord: 현재 컨텍스트 내의 식별자들에 대한 정보
    • outerEnvironmentReference: 외부 환경 정보
    • 변경 사항을 실시간으로 반영
    • let, const 변수 관리와 외부 환경에 대한 참조 유지 및 TDZ 관리
  3. ThisBinding
    • this 키워드가 참조하는 값을 결정
    • 실행 컨텍스트가 생성될 때 결정
    • 함수가 어떻게 호출되었는지에 따라 결정되는 this의 값

변수 환경과 다르게 렉시컬 환경은 변경 사항을 실시간으로 반영하는 이유는?

  • 변수 환경은 주로 var 키워드로 선언된 변수를 관리하는데, var 변수는 함수 스코프만 인식하고 호이스팅 시 undefined로 초기화되며 중복 선언을 허용한다. var는 블록 스코프를 무시하고 함수 스코프만 인식하기 때문에, 변수 환경은 복잡한 실시간 업데이트가 필요하지 않다.
  • 렉시컬 환경은 let, const를 위해 도입되었는데, let, const는 블록 스코프를 인식하고 TDZ가 적용되며 중복 선언이 불가하다. 블록 스코프마다 새로운 환경이 필요하고 TDZ를 정확히 관리해야하고 스코프 체인을 통한 변수 검색이 정확해야하기 때문에 실시간으로 변경사항을 반영해야한다.
  • 위와 같은 이유로 외부 환경을 참조하는 outer는 렉시컬 환경을 통해 검색이 이루어진다.

📌 실행 컨텍스트의 구성 요소가 어떻게 상호작용하는지 알아보기

let projectName = "움몀곰돔체";  // 렉시컬 환경에서 관리
var teamSize = 4;  // 변수 환경에서 관리

const team = {
  lead: "채멈",
  members: ["드뮴", "뭼주"],

  showStatus: function() {
    // this binding으로 team 객체 참조
    console.log(`${this.lead}이 대장인 ${projectName}`);

    // 화살표 함수에서는 외부 스코프의 this 유지
    const report = () => {
      console.log(`인원: ${this.members.length}`);
    };

    report();
  }
};

team.showStatus();
  • 변수 환경은 var로 선언된 teamSize를 관리한다.
  • 렉시컬 환경은 let으로 선언된 projectName과 const로 선언된 team을 관리한다.
  • this는 showStatus 내의 team 객체를 가리키고, 화살표 함수에서의 this는 외부 스코프의 this를 유지한다.

스코프 체인이란?

실행 컨텍스트 구조에는 outerEnvironmentReference가 있었다. 외부 환경을 참조하는 포인터로, 렉시컬 환경을 참조한다. 위에서 간단하게 적었듯이 렉시컬 환경은 실시간으로 변경 사항을 반영하기 때문에 외부 환경을 참조하는 환경은 렉시컬 환경이다.

스코프란?


스코프란 식별자에 대한 유효 범위를 의미한다. 예를 들어 스코프 A 외부에서 정의한 변수는 A의 외부/내부에서 모두 접근이 가능하다. 그러나 A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근이 가능하다.

변수를 어느 위치에 선언하느냐에 따라 변수를 사용할 수 있는 유효 범위인 스코프가 달라진다.

스코프 체인

식별자의 유효성을 검사하기 위해 안에서부터 바깥으로 유효 범위를 탐색해나가는 것을 스코프 체인이라 한다. 이를 가능하게 해주는 것이 outerEnvironmentReference이다.

outerEnvironmentReference는 호출된 함수가 선언된 당시의 렉시컬 환경을 참조한다.

var a = 1; // 전역 컨텍스트
function outer () { // outer 컨텍스트
  function inner () { // inner 컨텍스트
    console.log(a);
    var a = 3;
    console.log(a);
  }
  inner();
  // 1️⃣ outer의 렉시컬 환경을 outerEnvironmentReference로 참조
  console.log(a);
}
outer();
// 2️⃣ 전역 컨텍스트의 렉시컬 환경을 outerEnvironmentReference로 참조
console.log(a);
  1. outer 컨텍스트의 environmentRecord에서 'a'를 검색한다. 없기 때문에 outer의 참조를 통해 전역 렉시컬 환경으로 이동하고, 전역 environmentRecord에서 'a'를 검색해서 1을 출력한다.
  2. 전역 environmentRecord에서 'a'를 검색하고, 1을 발견하기 때문에 바로 1을 출력한다.

각 컨텍스트는 자신만의 environmentRecord를 가지고 있고, outer 참조를 통해 상위 스코프의 렉시컬 환경과 연결된다. 만약 자신의 environmentRecord에서 검색하는 값을 찾지 못한다면 상위로 이동해서 검색하게 된다.

변수 환경의 outerEnvironmentReference는 왜 있는걸까?

스코프 체인을 통해 변수를 검색할 때 렉시컬 환경의 outerEnvironmentReference를 통해 참조를 한다.

그렇다면 변수 환경의 outerEnvironmentReference는 왜 존재하는걸까?
현대 자바스크립트에서는 변수 환경의 outer는 거의 사용되지 않고 있다. 그러나 이를 완전히 제거하지 않은 이유는 기존 코드와의 호환성을 유지하고 var의 함수 스코프 특성을 보존하고 자바스크립트 엔진의 일관성 있는 구조를 유지하기 위해 여전히 존재한다.


호이스팅이란?

호이스팅은 자바스크립트에서 변수나 함수의 선언이 코드의 최상단으로 끌어올려지는 것처럼 동작하는 특징이다.

자바스크립트에서는 코드를 실행하기 전 식별자를 수집한다. 실제로 끌어올리는건 아니지만 끌어올리는 것으로 이해하는 가상의 개념이다. 식별자만 끌어올리고 할당 과정은 그대로 남겨둔다.

호이스팅을 통해 자바스크립트 엔진은 코드 실행 전에도 변수명들을 모두 알 수 있게 된다.


변수 호이스팅


var

console.log(name); // undefined
var name = "채멈";

위 코드는 다음과 같이 동작한다.

  • var name의 선언이 최상단으로 호이스팅된다.
  • console.log(name)를 실행하면 undefined를 출력한다.
  • 다음 줄의 name = "채멈"으로 할당이 실행된다.

let과 const

console.log(name); // ReferenceError
let name = "채멈";

위 코드는 다음과 같이 동작한다.

  • TDZ(Temporal Dead Zone)가 시작된다.
  • console.log(name)을 실행하면 변수는 초기화된 상태가 아니기 때문에 ReferenceError가 발생한다.
  • 다음 줄에서 name = "채멈"을 할당해서 TDZ가 종료된다.

TDZ란?

  • 일시적 사각지대(Temporal Dead Zone, TDZ)는 변수가 선언된 위치부터 초기화되기 전까지의 구간을 말한다. 이 구간에서는 변수에 접근할 수 없다.
  • 따라서 TDZ 구간에서 변수를 출력하려하면 ReferenceError가 발생하고 변수를 할당하는 위치에 오게 되면 TDZ가 종료된다.

var와 let, const의 차이

  • var는 선언과 초기화가 동시에 호이스팅되므로, 위로 끌어올려질 때 undefined로 초기화가 일어난다.
  • let, const는 호이스팅이 되지만 선언만 호이스팅되고 초기화는 실제 코드 위치에서 이루어지므로 초기화가 이루어지기 전 변수에 접근하면 에러가 발생한다.

let, const는 왜 등장했을까?

  • var로 선언된 변수는 호이스팅되어 변수를 선언하기 전에 접근할 수 있다. 이렇게 되면 코드의 가독성을 떨어뜨리고 예측할 수 없는 동작을 발생시킨다.
  • let, const는 블록 스코프로 선언된 블록 내에서만 유효하므로 변수 충돌을 줄여주고, 초기화되기 전의 구간에서 접근하면 ReferenceError를 발생시키기 때문에 실수로 초기화되지 않은 변수를 사용하는 것을 방지한다.

코드의 실행을 단계별로 살펴보기

위 코드를 실행하면 결과는 다음과 같다.

function a(x) {
  console.log(x); // 1
  var x;          // 이미 x가 있어서 무시됨
  console.log(x); // 1
  var x = 2;      // x에 2를 할당
  console.log(x); // 2
}
a(1);
  1. 호이스팅 단계

    function a () {
      var x;
      var x;
      var x;
    
      x = 1;
      console.log(x); // 1
      console.log(x); // 1
      x = 2;
      console.log(x); // 2
    }
    a();
    • x = 1로 초기화된다.
    • 이후 var x; var x = 2;가 호이스팅 되지만, 이미 매개변수 x가 있으므로 무시된다.
  2. 실행 단계

    function a(x) {    // x = 1
      console.log(x);  // 1 출력 (매개변수 값)
      var x;           // 이미 선언된 x가 있으므로 무시
      console.log(x);  // 1 출력 (매개변수 값)
      var x = 2;       // x에 새로운 값 2를 할당
      console.log(x);  // 2 출력 (새로 할당된 값)
    }

자바스크립트 엔진이 보는 코드

function a(x) {
  // 호이스팅된 상태
  // 매개변수 x가 이미 존재하므로 추가 var 선언은 무시

  console.log(x); // 매개변수 x의 값 1 출력
  // var x; 무시
  console.log(x); // 여전히 1
  x = 2;          // 값만 재할당
  console.log(x); // 2
}
a(1);
  • 함수 매개변수는 함수 스코프의 최상단에서 선언 및 초기화된다.
  • 함수 내부의 var 선언은 이미 존재하는 매개변수를 다시 선언하지 않는다.

함수 호이스팅


함수 선언문

sayHello(); // "안녕" 출력

function sayHello() {
  console.log("안녕");
}
  • 호이스팅되어 선언 전에도 사용이 가능하다.

함수 표현식

sayHello();

var sayHello = function() {
  console.log("안녕");
};
  • sayHello는 함수가 아니기 때문에 에러가 발생한다.
  • 함수 선언만 호이스팅되고 함수 할당은 호이스팅 되지 않는다.

화살표 함수

sayHello();

const sayHello = () => {
  console.log("안녕");
};
  • const로 선언되어 TDZ의 영향을 받는다.

코드의 실행을 단계별로 살펴보기

function a() {
  console.log(b);
  var b = 'bbb';
  console.log(b);
  var b = function () {}; 
  console.log(b);
}
a();
  1. 호이스팅 단계
    변수 선언과 함수 선언이 최상단으로 끌어올려진다.

    function a() {
      // 호이스팅
      var b; 
      function b() {}; 
    
      console.log(b);
      b = 'bbb';
      console.log(b);
      var b = function () {}; 
      console.log(b);
    }
  2. 실행 단계

    function a() {
      ...
      
      console.log(b); // undefined
      b = 'bbb';
      console.log(b); // 'bbb'
      var b = function () {}; 
      console.log(b); // [Function: b]
    }
    • 첫번째 console.log를 실행하면, 변수 선언이 함수 선언을 덮어씌우기 때문에 undefined가 출력된다.
    • b에 문자열 'bbb'가 할당되면 다음 console.log에는 문자열이 출력된다.
    • 그 다음 줄에서 b에 함수를 할당하게 되면, b는 함수를 참조하게 되므로 함수를 출력한다.

클래스 호이스팅


const myClass = new MyClass(); 

class MyClass {
  constructor() {
    this.name = "MyClass";
  }
}
  • 클래스는 let, const처럼 TDZ의 영향을 받는다.
  • 따라서 클래스를 선언하기 전에 사용하게 되면 ReferenceError가 발생한다.

함수는 초기화되지만 클래스는 지원하지 않는 이유


자바스크립트 엔진은 함수와 클래스를 다르게 처리한다.

함수 선언은 완전히 호이스팅 되므로 선언 전에 사용해줘도 되지만, 클래스의 경우는 호이스팅되지만 초기화되지 않는 상태로 남아있다. 따라서 클래스는 초기화되기 전까지 TDZ에 있기 때문에 이 구간에서 사용하게 되면 ReferenceError가 발생한다.

클래스는 왜 안되는걸까?

함수를 선언 전에 사용해도 되는 이유는 초기 자바스크립트의 설계 철학과 관련되어 있다.

function init() {
  setup();
	loadData();
  registerEvents();
}

function setup() { }
function loadData() { }
function registerEvents() { }
  • 위 코드처럼 주요 로직을 위에 두고, 보조 함수들을 아래에 두는 패턴이 많이 사용된다.
  • 위 패턴 뿐만 아니라 상호 재귀 호출이 지원이 되기 때문에 유용하다.
  • 현대 자바스크립트에서는 함수 선언을 상단에 배치한다. 호이스팅에 의존하지 않는 코드는 가능하지만 권장하지는 않는 것이다.
    • 그럼에도 계속해서 선언 전에도 사용할 수 있게 유지하는 것은 기존 코드가 호이스팅에 의존할 수 있고, 갑자기 제거해서 동작하지 않는 경우가 있을 수 있기 때문이다.
    • 또한 상호 재귀나 코드 구조화의 유연성의 유용한 사례가 존재한다.

☑️ 요약 정리

자바스크립트 실행 과정

  1. 생성 단계: 실행 컨텍스트가 생성되고 environmentRecord 생성, 외부 환경 참조 설정, this 바인딩이 이루어진다.
  2. 실행 단계: 코드가 순차적으로 실행된다.

실행 컨텍스트

코드 실행에 필요한 모든 정보를 담고 있다.

  1. 변수 환경: var 변수를 관리하며 초기 상태를 스냅샷처럼 유지한다.
  2. 렉시컬 환경: let, const 변수를 관리하며 실시간으로 변경사항을 반영한다.
  3. ThisBinding: 함수가 어떻게 호출되었는지에 따라 this 값을 결정한다.

스코프 체인

식별자 검색은 현재 스코프에서 시작해 상위 스코프로 연결되는 스코프 체인을 따라 이루어진다. 렉시컬 환경의 outer 참조를 통해 구현된다.

호이스팅 작동 방식

호이스팅은 선언을 코드 최상단으로 끌어올리는 것처럼 동작한다.

  • var: 선언과 초기화가 동시에 이루어져 undefined로 초기화된다.
  • let, const: 선언만 되고 TDZ 영향을 받아 초기화 전에 접근하면 에러가 발생한다.
  • 함수 선언문: 전체가 호이스팅되어 선언 전에도 사용 가능하다.
  • 클래스: let, const처럼 TDZ 영향을 받아 선언 전 사용이 불가능하다.

호이스팅을 공부할 때 변수와 함수의 호이스팅의 차이는 무엇인가?에 대해서만 공부했었다. 이렇게 공부하니 실제 코드 예제가 주어졌을 때 호이스팅이 발생하고 어떻게 출력되는지는 이해하기가 어려웠다. 이번에 호이스팅을 공부하며 자바스크립트의 동작 원리와 스코프 체인까지 함께 공부해서 제대로 이해할 수 있었다.

📝 참고 자료


profile
안녕하세오

0개의 댓글

관련 채용 정보