본문 바로가기
Javascript

Javascript 실행 컨텍스트, 호이스팅, 클로저의 이해(1)

by 밥바비 2023. 2. 9.
반응형

자바스크립트를 공부하며 이해하는데 꽤나 오랜 시간이 걸렸던 개념이 바로 실행 컨텍스트였습니다. 이번글을 통해 ES3와 ES5에서의 실행 컨텍스트에 대해 정리해보려고 합니다. 굳이 옛날 스펙인 ES3까지 다루는 이유는 저의 경우 실행 컨텍스트를 이해하는데 ES3의 스펙이 많은 도움이 됐기 때문입니다. 또 스펙 간의 변화를 느껴보는 것도 새로운 경험인 것 같아 정리를 해보려고 합니다.

 

실행 컨텍스트는 자바스크립트 동작 원리와 호이스팅, 클로저를 이해하는데 많은 도움이 되니 조금 어렵더라도 차분히 읽어보시긴 권해드립니다! 본 내용은 총 2편에 걸쳐 작성될 예정입니다.

 

1편 - 실행 컨텍스트의 개념, ES3 기반의 실행 컨텍스트

2편 - ES5 기반의 실행 컨텍스트, 실행 컨텍스트로 이해하는 호이스팅과 클로저

실행 컨택스트란

실행 컨텍스트는 간단하게 코드의 실행 환경이라고 할 수 있습니다. 자바스크립트 코드가 자바스크립트 엔진에 의해 실행되기 위해선 아래와 같은 정보들이 필요합니다.

  • 변수: 전역번수, 지역변수, 매개변수, 객체의 프로퍼티
  • 함수 선언
  • 변수의 유효범위(Scope)
  • this

그리고 이러한 정보들이 기록되어 있는 것이 실행 컨텍스트 입니다. 실행 컨텍스트는 전역 코드, 함수 코드, eval 코드에 의해 생성되고(이를 실행 가능한 코드라고 합니다.), 엔진은 실행 컨텍스트가 생성되고 소멸하는 과정을 통해 전체 코드를 실행해 나가게 됩니다.

실행 컨텍스트는 후입선출(LIFO)의 구조인 스택의 형태로 관리되는데, 이를 실행 컨텍스트 스택(Execution Context Stack)이라고 합니다. 실행 컨텍스트의 생성과 소멸 과정을 순서대로 나열해 보면 아래와 같습니다.

function foo() {
  console.log("foo...");

  function bar() {
    console.log("bar...");
  }

  bar();
}

foo();

실행 컨텍스트 순서 이미지
스택 형태로 관리되는 실행 컨텍스트

  1. 코드 실행 중 실행 가능한 코드를 만나게 되면 새로운 실행 컨텍스트가 생성되고 스택의 상단에 쌓입니다.
    • 전역 코드: 엔진이 <script> 태그를 만나는 순간 전역 실행 컨텍스트가 생성되고 전역 실행 컨텍스트는 웹 페이지에서 나가거나 브라우저가 닫힐 때 까지 유지됩니다.
    • 함수 코드: 함수가 실행 될 때 함수 실행 컨텍스트가 생성되고 해당 함수가 종료될 때 까지 유지됩니다.
  2. 스택의 상단에 새로운 실행 컨텍스트가 쌓이면 제어권이 새롭게 생성된 실행 컨텍스트로 이동합니다.
  3. 함수 실행이 끝나면 해당 함수의 실행 컨텍스트가 소멸되고 제어권이 직전 실행 컨텍스트로 이동합니다.

여기까지의 내용을 봤을 때 정리해 볼 수 있는 내용은 이렇습니다.

  • 실행 컨텍스트는 자바스크립트 코드가 실행되는데 필요한 정보들을 담고 있는 친구고, 이 친구는 특정 상황에서 생성된다. 이렇게 만들어진 친구들은 스택에 차곡차곡 쌓이면서 맨 위에 있는 친구가 차례로 실행되고 실행이 끝나면 그 밑에 있던 친구가 다시 실행의 주체가 된다.
반응형

실행 컨텍스트의 구조와 생성과정(ES3)

다음은 ES3 기반의 실행 컨텍스의 구조와 생성과정을 정리한 내용입니다.

실행 컨텍스트의 구조

실행 컨텍스트 물리적으로 객체의 형태를 가지며, 아래 3가지의 프로퍼티를 가지고 있습니다.

Execution Context{
  Variable Object (변수객체),
  Scope Chain (스코프 체인),
  thisValue (this)
}
  • Variable Object(VO) : 변수, 매개변수(parameter)와 인수(arguments), 함수 선언 정보를 저장하는 객체를 가리킴
  • Scope Chain(SC) : 현재 변수 객체 + 중첩된 함수의 스코프 리스트를 차례로 저장
  • thisValue : 함수 호출 패턴에 의해 할당된 this

위에서 전역 코드와 함수 코드가 각각 전역 컨텍스트, 함수 컨텍스트를 생성한다고 했던 내용 기억하시나요? 전역 컨텍스트와 함수컨텍스트의 기본 구조는 동일합니다. 그러나 내부에서 관리하는 값에 약간의 차이가 존재합니다.

아래 예시 코드를 통해 차이를 알아보겠습니다.

var x = "xxx";

function foo(a) {
  var y = "yyy";
  console.log(y, a);
}

foo("aaa");

전역 컨텍스트

예시 코드에서 생성되는 전역 컨텍스트를 객체의 형태로 나타내면 아래와 같습니다.

"전역 컨테스트": {
  "VO" : "전역 객체",
  "Scope chain" : ["전역 객체"],
  "this": "전역 객체" // --> window
}

"전역 객체": {
  x : undefined,
  foo : function object
}

생각보다 단순하게 전역 컨텍스트는 각 프로퍼티가 전부 전역 객체(Global Object)를 가리키고 있습니다. 그리고 전역 객체는 따로 인수(arguments)가 없기 때문에 전역 스코프에 존재하는 변수와 함수의 정보만을 담고 있습니다.
전역 컨텍스트는 실행 컨텍스트 스택의 최하단에 위치하며 더 상위의 중첩된 스코프가 없기 때문에 전역 객체의 레퍼런스(참조)만을 포함하고 있습니다.

전역 객체는 브라우저의 경우 `window` , NodeJS의 경우 `global` 을 의미합니다.
전역 객체의 내부에는 빌트인 객체(Math, String, Array 등)와 BOM, DOM이 설정되어 있습니다.

함수 컨텍스트

"foo 함수 컨테스트": {
  "VO" : "foo 활성화 객체",
  "Scope chain" : ["foo 활성화 객체", "전역 객체"],
  "this": "전역 객체" // --> window
}

"foo 활성화 객체": {
  arguments: [{a : "aaa"}]
  y : undefined
}

전역 컨텍스트와의 차이가 보이시나요??

  • 함수 컨텍스트의 변수 객체는 함수의 인수 리스트인 arguments를 포함하는 활성화 객체(Activation Object)를 가리키고 있습니다.
  • 스코프 체인은 자신의 활성화 객체를 시작으로, 자신이 포함된 전역 컨텍스트의 스코프 체인이 더해진 것을 확인할 수 있습니다.
  • this는 함수의 호출 방식에 따라 결정되는데, 위 예제에선 일반 함수 호출을 통해 호출됐기 때문에 `window`가 할당됐습니다.

예를 들어 `foo.func()`와 같이 호출된다면 `this`는 `foo`로 바인딩 됩니다.

실행 컨텍스트의 생성과정

이제 실행 컨텍스트가 어떻게 생겼는지 알았습니다. 이제 이 친구가 어떤 방식으로 생성되는지 알아봅시다! 실행 컨텍스트는 아래 3단계의 순서로 생성됩니다.

  1. 스코프 체인의 생성과 초기화(SC와 연관된 순서)
  2. 변수 객체화 실행(VO와 연관된 순서)
  3. this value 결정(this와 연관된 순서)

스코프 체인의 생성과 초기화

이 과정에서는 스코프 체인이 생성되고 각 컨텍스트에 맞는 스코프 체인이 초기화됩니다.

"foo 함수 컨테스트": {
  "VO" : uninitialized
  "Scope chain" : ["foo 활성화 객체", "전역 객체"],
  "this": uninitialized
}

"foo 활성화 객체": uninitialized

변수 객체화 실행

변수 객체에 프로퍼티와 값을 설정하는 과정입니다. 해당 과정은 아래와 같은 순서로 진행됩니다.

  1. 매개변수를 프로퍼티, 인수를 값으로 설정(함수 컨텍스트의 경우)
  2. 해당 코드 내의 함수 명을 프로퍼티, 함수 객체를 값으로 설정
  3. 해당 코드 내의 변수 명을 프로퍼티, undefined를 값으로 설정
"foo 함수 컨테스트": {
 "VO" : "foo 활성화 객체",
 "Scope chain" : ["foo 활성화 객체", "전역 객체"],
 "this": uninitialized
}

"foo 활성화 객체": {
 arguments: [{a : "aaa"}], // ...1
 bar : function Object,    // ...2
 y : undefined,            // ...3
}

this value 결정

함수 호출 패턴에 의해 결정, 전역 컨텍스트의 경우 전역 객체

"foo 함수 컨테스트": {
 "VO" : "foo 활성화 객체",
 "Scope chain" : ["foo 활성화 객체", "전역 객체"],
 "this": "전역 객체" // --> window
}

"foo 활성화 객체": {
 arguments: [{a : "aaa"}]
 y : undefined,
 bar : function Object
}

변수 객체화 실행의 2번 과정 중 함수 처리에 대해 좀 더 자세히 살펴보겠습니다.

함수 객체가 값으로 할당될 때, 생성된 함수 객체는 [[Scopes]] 프로퍼티를 가지고 있습니다. 이는 함수 객체가 실행되는 환경을 가리킵니다. 다시 말해 [[Scopes]] 스코프는 해당 컨텍스트의 Scope Chain을 가리킵니다.

context scope 도식화 이미지
Context Scope

전역 컨텍스트의 경우 전역 객체가 먼저 생성된 상태에서 스코프 체인 생성과 초기화가 이루어집니다.
반면, 함수 컨텍스트는 스코프 체인이 생성되고 활성화 객체를 생성합니다. 그 후 생성된 활성화 객체를 스코프 체인의 선두에 설정하고 상위 스코프의 스코프 체인을 차례로 push 합니다.

코드의 실행

꽤나 복잡한 과정을 통해 실행 컨텍스트가 생성됐습니다. 여기까지가 코드 실행의 사전 작업입니다.
현재까지의 단계에서는 변수에 아직 undefined가 할당되어 있습니다. 이후 코드가 차례로 실행됨에 따라 해당 변수의 할당문을 만나게 되면, 그제서야 변수에 값이 할당됩니다. 그러나 함수의 경우 실행 컨텍스트가 생성될때 부터 함수 객체가 할당 됨으로 실행 컨텍스트가 생성된 이후에는 해당 코드 내 어디서든 실행될 수 있습니다.

이런 내부 동작으로 인해 호이스팅이란 현상이 발생하게 됩니다! 이후에 좀 더 자세히 설명하도록 하겠습니다.

아래는 위의 예제코드에서 할당문이 모두 실행된 후의 실행 컨텍스트를 나타냅니다.

"전역 컨테스트": {
  "VO" : "전역 객체",
  "Scope chain" : ["전역 객체"],
  "this": "전역 객체" // --> window
}

"전역 객체": {
  x : "xxx",
  foo : function object
}


"foo 함수 컨테스트": {
 "VO" : "foo 활성화 객체",
 "Scope chain" : ["foo 활성화 객체", "전역 객체"],
 "this": "전역 객체" // --> window
}

"foo 활성화 객체": {
 arguments: [{a : "aaa"}]
 y : "yyy",
 bar : function Object
}

또 다른 예시

var x = "xxx";

function foo() {
  var y = "yyy";

  function bar() {
    var z = "zzz";
    console.log(x + y + z);
  }
  bar();
}

foo();

위 코드가 실행되면 최종적으로 아래와 같은 실행 컨텍스트들이 생성됩니다!!

예시 코드의 실행컨텍스트 생성 도식화
예제 코드의 실행컨텍스트

반응형

댓글