프로그래밍 언어/JavaScript

JavaScript | 실행 컨텍스트 (1) - 실행 컨텍스트, 호이스팅, 스코프

hyuga_ 2024. 3. 26. 21:26

1. 실행 컨텍스트(Execution Context)

실행 컨텍스트란?

흔히 JS의 동작원리를 그릴 때 콜 스택에 함수가 쌓이는 식으로 표현하는데, 그 쌓이는 것이 실행 컨텍스트이다. 지금 실행할 스크립트에 대한, 여러가지 필요한 정보(환경)를 객체에 담은 것이라고 보면 된다. 각 실행 컨텍스트는 변수 객체(Variable Object), 스코프 체인(Scope Chain), 그리고 this에 대한 정보를 포함한다.

 

실행 컨텍스트는 자바스크립트 엔진의 가장 핵심적인 부분 중 하나로, 스코프, 호이스팅, 클로저 같은 자바스크립트의 주요 동작 원리의 기반이 되는 개념이다. 

 

실행 컨텍스트의 생성

실행 컨텍스트는 우선 1) 전역 실행 컨텍스트(Global Execution Context), 2) 함수 실행 컨텍스트(Function Execution Context)로 나눌 수 있다. 이 중에 우리가 생성할 수 있는 건 함수 실행 컨텍스트 뿐이다.

 

전역 실행 컨텍스트는 JS가 실행하는 순간 바로 콜 스택에 담긴다. 실제 코드 내용이 실행되기 전 브라우저에서 자동으로 실행하므로, JS 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 된다. 그리고 전역 컨텍스트는 런타임이 종료될 때까지 유지된다.

 

전역 컨텍스트를 구성하고있는 정보는 1) 전역 변수, 2) 전역 객체(브라우저의 경우 window 객체, Node.js 환경일 경우 global 객체)이다. 밑에서 다루겠지만, 함수 컨텍스트는 JS 엔진이 코드 실행에 필요한 정보들을 수집하는 방식인데, 전역 컨텍스트는 브라우저 또는 Node.js 환경에서 별도로 제공하는 객체를 사용한다. (따라서 이들은 JS 내장 객체(native object)가 아닌 호스트 객체(host object)로 분류된다)

 

이전에 분명 이벤트루프는 자바스크립트의 콜 스택이 비었는지 확인하고, 비었다면 마이크로태스크 큐, 혹은 태스크 큐에서 비동기 작업의 결과를 처리할 콜백함수들을 콜 스택에 추가하는 거라고 배웠다. 그런데 전역 컨텍스트가 런타임이 종료될 때까지 유지되는 거라니? 오해의 여지가 있는 표현이다. 실제로는 함수 컨텍스트가 비었고, 전역 컨텍스트만 남았을 때를 흔히 '콜 스택이 비었다'고 표현한다고 한다.

 

 

이후 특정 함수가 호출될 때마다 해당 함수에 대한 환경 정보를 수집해서 실행 컨텍스트를 구성하고 콜스택에 담는다. 만일 아래와 같은 코드가 실행된다면, 콜 스택은 다음과 같이 동작한다. 

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

 

출처: 코어 자바스크립트

 

 

실행 컨텍스트의 구조

어떤 실행 컨텍스트가 '활성화'될 때, JS 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 실행 컨텍스트 객체에 저장한다. 해당 객체에 담기는 정보들은 다음과 같다. (사실 객체라고 표현하는 게 맞는지는 모르겠다. 명세상에는 객체가 아닌 추상적인 자료구조로 표현된다. 그러나 어차피 서비스 개발자가 뜯어보거나 조작할 영역은 아니어서 객체라고 이해하는 것이 편의상 좋다고도 한다.) 이 정보들은 호이스팅, 스코프, this 를 수행함에 있어 필수적인 정보들이다. 

 

1) VariableEnvironment(변수 환경)

  • 선언 시점의 LexicalEnvironment의 스냅샷으로, 이후 변경 사항은 반영되지 않는다.
  • 담기는 정보
    • environmentRecord(환경 레코드): 현재 컨텍스트 내의 식별자들에 대한 정보 (스냅샷)
    • outerEnvironmentReference(외부 환경 참조): 외부 환경 정보 (스냅샷)

2) LexicalEnvironment(렉시컬 환경)

  • 처음에는 VariableEnvironment의 사본. 이후 변경 사항이 실시간으로 반영된다. 동작과정에서 주로 활용되는 것은 이 렉시컬 환경이다.
  • 담기는 정보
    • environmentRecord(환경 레코드): 현재 컨텍스트 내의 식별자들에 대한 정보 -> 호이스팅과 관련
    • outerEnvironmentReference(외부 렉시컬 환경 참조)외부 환경 정보 -> 스코프 체인, 클로저 현상와 관련

3) ThisBinding

  • this 식별자가 바라봐야 할 대상 객체를 기록한다.

 

ThisBinding은 후에 this를 살펴볼 때 다루도록 하고, 또한 초기화 과정 이후에는 변수 환경이 아닌 렉시컬 환경이 주로 쓰이므로 렉시컬 환경을 위주로 살펴보면서, 호이스팅과 스코프 개념에 대해서 함께 이해해보자.

 

 

2. 호이스팅(hoisting)

선정리 후설명

호이스팅은, 렉시컬 환경의 환경 레코드가 매개변수의 식별자, 함수 내부에서 선언된 변수의 식별자, 함수 내부에서 선언된 함수 전체(선언부와 할당부)를 미리 인식하고 시작함으로써, 코드가 실행되는 순간부터 해당 식별자들이 선언되는 효과를 갖는 현상이다. 따라서 선언 전에 해당 변수와 함수에 접근할 수 있다.

let과 const 역시 호이스팅이 일어나지만, var와는 달리 Temporal Dead Zone(TDZ)으로 인해 선언 전에 접근하려 하면 ReferenceError가 발생한다.

함수의 경우, 함수 선언 방식이라면 변수와 달리 함수 내용 전체가 호이스팅되나, 함수 표현식을 사용하면 변수와 마찬가지로 선언부만 호이스팅된다.

 

환경 레코드(environmentRecord)와 호이스팅

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

 

컨텍스트 내부 전체를 처음부터 끝까지 순서대로 훑으며 다음 정보들을 수집한다

  1. 현재 컨텍스트 내에서 선언된 모든 지역 변수, 함수 선언, 매개변수 등의 식별자 정보
    • 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자
    • 함수 내에서 선언된 변수의 식별자 (var, let, const의 주요 차이점이 여기서 나온다 -> 선언 전 접근 가능 여부)
  2. 선언한 함수가 있을 경우 그 함수 자체
    • 함수 표현식으로 선언했을 경우 함수의 내용은 수집되지 않는다.

변수나 함수의 선언부를 미리 끌어올린다고 이해하면 된다.

 

 

변수 호이스팅

만약 다음 코드가 실행된다고 하자. 

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

 

호이스팅에 대한 개념을 배제한다면, 위 코드의 출력 결과는 1, undefined, 2 가 나올 것 같다. 그러나 실제로는 1, 1, 2가 출력된다. 왜일까?

 

처음 렉시컬 환경을 구성할 때, 환경 레코드는 다음 정보들을 수집한다. '매개변수 x가 있고, a 함수 내부에서 선언된 변수 x가 있구나!'

코드를 본격적으로 실행하기 전에 해당 정보를 알고 코드를 실행한다. 따라서 엔진 입장에서는 위 코드는 아래와 똑같이 느껴진다.

 

function a() {
    var x; // 매개변수
    var x; // 함수 내 선언 1
    var x; // 함수 내 선언 2

    x = 1; // 매개변수 값 할당
    console.log(x);
    console.log(x);
    x = 2; // 함수 내 값 할당
    console.log(x);
}
a(1);

 

 

함수 호이스팅

hello(); // "Hello, world!"

function hello() {
    console.log("Hello, world!");
}

 

 

hello 라는 함수를 선언했다. 이러한 방식의 경우 함수 블록 내부도 선언부의 일부로 처리되므로, 함수 내용 전체가 호이스팅된다. 따라서 선언 전에 호출하더라도 에러가 아니라 함수가 정상 작동된다. 

 

hello(); // TypeError: hello is not a function

var hello = function () {
    console.log("Hello, world!");
};

 

그러나 함수 표현식을 사용하면, 선언부와 할당부가 분리된다. 따라서 변수와 마찬가지로 선언부(var hello)만 호이스팅된다. 때문에 미리 호출하면 타입 에러를 반환한다. (아직 해당 식별자가 함수를 참조하고 있지 않은데, 함수를 호출하겠다고 했으므로)

 

이는 코드를 좀 더 예측 가능하게 만들어준다. 예를 들어 방대한 코드를 여러명의 개발자가 협업하고 있으며, 모두 함수 선언식을 사용한다고 하자. 이때 누군가의 부주의로 위에서 선언한 함수와 동일한 이름의 함수를 다시 선언하게 된다면, 해당 지점부터 아래 코드만 영향을 받는 것이 아니라 호이스팅에 의해 위의 함수가 덮어씌워져버린다. 이전에는 잘 돌아가던 코드가 해당 함수의 output이 달라짐으로 인해서 예기치 못한 부분에서 문제가 터질 수 있는 것이다. 만일 함수 표현식을 사용했다면 아마 에러를 출력했을 것이므로 이러한 문제점을 사전에 알 수 있었을 것이다. 

 

흔히 호이스팅을 설명할 때 변수 선언부나 함수를 말 그대로 '끌어올린다'는 식으로 설명하고는 하는데, 내가 읽은 책에 따르면 그것은 사실이 아니다. 자바스크립트가 받아들이기에 그것과 다름없이 동작하는 건 맞으나, 어디까지나 호이스팅 현상을 직관적으로 표현한 것일 뿐이지, 실제로 코드를 끌어올리는 코드 변환과정이 수행되진 않는다.

 

 

 

3. 스코프(Scope)

선정리 후설명

스코프는 식별자에 대한 유효 범위, 즉 변수나 함수가 접근 가능한 범위이다. 스코프는 전역 스코프와 지역 스코프로 나눌 수 있고, ES6부터 지역 스코프는 다시 함수 스코프와 블록 스코프로 나뉠 수 있다. 만일 특정 식별자로 접근할 때 해당 스코프에서 참조할 대상을 찾지 못한다면, outerEnvironmentReference를 따라 상위 스코프를 탐색하고, 이를 대상을 찾을 때까지 (최대 최상위 스코프인 전역 스코프까지) 반복한다. 이 과정을 스코프 체인이라고 부른다.

 

 

스코프란?

스코프란 식별자에 대한 유효범위, 즉 변수나 함수가 접근 가능한 범위이다. 따라서 스코프는 코드 내에서 변수와 함수의 가시성과 생명주기를 결정한다.

 

스코프의 종류는 다음과 같다.

  • 전역 스코프(Global Scope): 전역 스코프에 선언된 변수는 어플리케이션의 어디서든 접근할 수 있다. 이 변수들은 전역 실행 컨텍스트에 저장된다.
  • 지역 스코프(Local Scope)
    • 함수 스코프: 함수 내부에서 선언된 변수는 지역 스코프를 가지며, 해당 함수 내부에서만 접근할 수 있다. 함수가 실행될 때마다 해당 함수의 스코프가 생성된다.
    • 블록 스코프 (ES6+의 let과 const): let과 const를 사용하여 선언된 변수는 블록 스코프를 가진다. 이는 대괄호 {}로 둘러싸인 모든 영역(ex. if문, for문)에서 유효하다. 블록 스코프 변수는 오직 해당 블록 내부에서만 접근 가능하며, 외부에서는 접근할 수 없다.

 

외부환경참조(outerEnvironmentReference)와 스코프 체인

스코프 체인은 코드에서 변수에 접근하기 위한 '연결 리스트'와 같다. 각 실행 컨텍스트는 자신만의 스코프를 가지며, 이 스코프는 부모 스코프와 연결된다. 변수를 찾을 때, 자바스크립트 엔진은 현재 스코프에서 시작하여 상위 스코프 방향으로 이동하며 해당 변수를 검색한다. 이 과정은 가장 가까운 변수를 찾거나, 최상위 스코프인 전역 스코프에 도달할 때까지 계속된다. 역방향(상위에서 하위로)은 성립하지 않는다.

 

앞서, 어떠한 실행 컨텍스트의 렉시컬 환경 구성요소로 EnvironmentRecord와 outerEnvironmentReference가 있다고 했다. 이중 외부환경참조(outerEnvironmentReference)는 스코프 체인을 위해 존재한다. 

 

외부환경참조는 현재 호출된 함수가 '선언될 당시'의 렉시컬 환경을 참조한다. 선언될 당시의 환경은 즉, 해당 함수를 호출한 환경을 의미한다. 예를 들어 A 함수 내부에 B 함수를 선언했다면, B 함수의 외부환경참조는 B 함수가 선언되던 때의 A 함수의 렉시컬 환경을 참조한다. 그리고 A 함수는 아마 전역 컨텍스트의 렉시컬 환경을 참조하고 있을 것이다. 

 

(참고로 선언될 당시에 스코프가 정해진다고 해서, 자바스크립트의 스코프를 정적 스코프, 즉 렉시컬 스코프라고 부르기도 한다.)

 

 

출처: 코어 자바스크립트