[시리즈 - 코어 자바스크립트] 자바스크립트 실행 컨텍스트

내실을 다지는 자바스크립트 공부

hankyeolk

이 블로그 콘텐츠에는 책 ‘코어 자바스크립트’를 읽고 자바스크립트를 깊게 이해하는 내용이 담깁니다.

자바스크립트 실행 컨텍스트

실행 컨텍스트는 실행할 코드에 제공할 환경 정보를 모아놓은 객체다. 어떤 실행 컨텍스트가 활성화되는 시점에 어떤 변수가 끌어올려지고, 외부 환경 정보를 설정하고, this 값을 설정하는 등의 동작을 수행한다.

동일 환경에 있는 코드를 실행할 때 필요한 환경 정보를 모두 모아 컨텍스트로 구성하고, 이를 콜 스택에 쌓아올린다. 여기서 스택은 자료구조의 ‘그’ 스택이 맞다. 가장 위에 쌓은 컨택스트와 관련있는 코드를 실행하는 식으로 전체 코드의 환경과 순서를 정한다.

하나의 동일한 환경, 하나의 실행 컨텍스트를 구성할 수 있는 방법은 다양하게 있다. 전역공간에서의 작업, eval() 함수의 활용, 가장 흔한 방법은 함수를 실행하는 것이다.


실행 컨텍스트 스택의 기본 동작

// -- (1)
var a = 1;

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

outer(); // -- (3)
console.log(a); // 1

위의 코드에서 (1)의 환경은 전역 컨텍스트다. 코드가 읽히는 순간 가장 먼저 콜 스택에 담긴다. 전역 컨텍스트는 별도의 실행 명령 없이도 브라우저에서 자동으로 실행하는 컨텍스트다. 말 그대로 파일이 브라우저에서 열리는 순간 전역이 펼쳐지는 것이다.

전역적으로 코드들을 담다가, (3)에서 함수 호출을 만나면 outer 함수에 대한 환경 정보들을 컨텍스트로 생성하여 콜 스택에 담는다. outer() 함수의 실행 컨텍스트를 콜 스택에 담는 것이다. 이제 전역에서 시행하는 것을 멈추고 outer 함수 내부의 코드들을 순차적으로 실행시킨다.

outer() 함수 내부에서 inner() 함수를 호출하는 (2)번 환경이 있기 때문에 콜 스택은 해당 inner 함수의 환경을 실행 컨텍스트로 담는다. 스택의 특성상 가장 뒤에 들어온 요소를 가장 먼저 처리해야하기 때문에 inner가 처리되고, outer, 다시 전역 순서로 진행한다.

하나의 실행 컨텍스트가 콜 스택의 가장 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점이다. 새로운 스택으로 특정 컨텍스트가 활성화되면 해당 코드를 실행하는 것에 필요한 환경 정보를 수집한 객체가 실행 컨텍스트에 저장되는 방식이 자바스크립트의 동작 방식이다.


실행 컨텍스트 객체 엘리먼트

실행 컨텍스트 객체는 자바스크립트 엔진이 코드를 돌리는 목적으로 생성되기 때문에 우리가 코드를 통해서 직접 확인하기는 힘들다.


Variable Environment (변수 환경)

현재 컨택스트가 가지고 있는 식별자에 대한 정보, 외부 환경 정보, 선언 시점의 Lexical 환경의 스냅샷을 가지는 환경이다. 해당 컨텍스트가 최초로 실행될 때의 스냅샷을 유지한다. 이 스냅샷을 그대로 복사해서 Lexical 환경을 만든다. 실핵 컨텍스트의 템플릿을 만드는 격이라고 생각하면 편하겠다.


Lexical Environment (사전적 환경)

처음 선언될 때에는 Variable 환경과 동일하지만 변경사항이 생기면 실시간으로 반영되는 환경이다. “현재 컨텍스트에는 어떤 어떤 식별자들이 있고, 외부 환경에 대한 정보는 특정 어떤 주소를 참조하도록 되어있다.” 이런 형식으로 사전처럼 정의된 환경이다.

environmentRecord

environmentRecord에는 현재 컨텍스트와 관련된 코드 식별자 정보가 저장된다. 함수의 매개변수 이름, 함수 그 자체, 선언된 변수가 컨텍스트의 식별자가 된다. 코드 위에서 아래로 훑으며 순서대로 수집한다.

수집된 식별자들은 호이스팅을 통해서, 자바스크립트 엔진이 실질적으로 식별자들을 실행의 가장 상단으로 끌어올리지는 않지만, 코드 해석의 최상단으로 끌어올려진다.


호이스팅 (hoisting = hoist + ing) 규칙

  • 특정 실행 컨텍스트에 대해서 변수는 선언만 끌어올린다. 할당은 끌어올리지 않는다. 매개변수로 들어오는 값도 동일하다.
  • 함수에 대한 선언은 함수 전체를 끌어올린다. 동일 변수명으로 함수를 선언하면 해당 변수명의 주소값에 함수만 담겨 끌어올려진다고 생각하자. 다만, 표현식으로 작성된 함수는 끌어올려지지 않는다. 할당의 개념이기 때문이다.

함수 선언과 표현식

함수를 정의하는 방법은 아래의 3가지 방식을 따른다. 선언식의 경우 보통 function 키워드로 선언하는 부분만 존재하고 별도의 할당 과정이 없다. 함수 선언식의 경우에는 반드시 함수명이 정의되어야 한다.

이와 반대로 함수 표현식은 정의한 함수를 별도 변수에 할당하는 과정 자체를 의미한다. 표현식으로 작성된 함수는 별도의 이름이 꼭 존재하지는 않아도 된다. 익명함수라고도 한다.

function a() {} // 함수 선언식

const b = function () {}; // 함수 표현식 -> function 키워드
const d = function e() {}; // 이름이 있는 함수 표현식 -> 외부에서 해당 함수명으로 호출 불가
e(); // -> error

const c = () => {}; // 함수 표현식 -> arrow function


스코프

스코프는 식별자, 변수가 접근 가능한 유효범위다. ES6부터 자바스크립트는 함수, 블록에 대한 스코프가 구분되었다. 식별자의 유효범위가 안에서부터 바깥으로 순서대로 검색하는 체인을 ‘스코프 체인’이라고 한다. 자바스크립트에서 스코프는 중괄호로 구분한다. 또한 함수의 내부에 식별자가 있는지 외부에 있는지도 스코프를 따지는 것에 큰 영향을 준다.

스코프의 규칙을 정리하면 다음과 같다.

  • 내부에 있는 스코프에서 외부의 스코프로의 접근은 가능하다. 즉, 내부 스코프에서 외부에 정의된 변수를 조회할 수 있고, 사용할 수 있다.
  • 외부 스코프에서는 내부에 정의된 식별자를 조회할 수 없다. 이것을 통해서 우리는 변수의 은닉화를 할 수 있다.
  • 스코프는 중첩이 가능하다. 중첩된 가장 외부의 스코프를 전역(global), 그것이 아닌 스코프는 모두 지역(local) 스코프다.
  • 지역 스코프 내에서 선언된 식별자, 변수가 Lexical 환경에서 조회되기 때문에 우선순위를 가진다. Lexical한 환경에서 선언된 식별자가 없다면 그 다음 스코프를 뒤지고 또 다음 스코프를 뒤지는 형식으로 가는 것이다.