조각조각 - 실행 컨텍스트

eocode·2024년 2월 24일
1
post-thumbnail

목차

  1. 실행 컨텍스트
    1.1. 실행 컨텍스트란?
    1.2. 실행 컨텍스트 유형
    1.3. 실행 컨텍스트 구성
    1.4. 자바스크립트 엔진 동작과 실행 컨텍스트 생명 주기
    1.5. 변수 환경엔 왜 var 타입 변수와 함수 선언식만?
  2. 참고자료

클로저를 공부하려다 보니 실행 컨텍스트가 등장하고 실행 컨텍스트를 찾아보니 이벤트 루프, 콜스택, 스코프 체인, 호이스팅 등등의 기존에 들어보았지만 간단하게만 이해하고 넘어간 것들이 계속 등장 하였습니다. 따라서 이번엔 개념에 집중하고자 연관되어 있는 개념들을 총정리 해보도록 하겠습니다.

실행 컨텍스트

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

실행 컨텍스트는 자바스크립트 코드가 실행되는 환경을 추상화한 개념으로 자바스크립트 엔진이 코드를 실행하기 위해 필요한 모든 정보를 담고 있는 객체입니다. 함수가 실행되면 콜스택에 실행 컨텍스트가 추가되고 함수가 완료되면 콜스택에서 실행 컨텍스트가 제거되는 식으로 처리됩니다.

간단히 정리하자면 실행 컨텍스트는 콜스택에 담기는 코드 실행에 필요한 환경 덩어리입니다.

콜스택 (실행 컨텍스트 스택)

자바스크립트 엔진은 콜스택을 이용해 함수 호출을 관리합니다. 콜스택은 이름 그대로 스택 구조를 이루고 있어 LIFO(Last In, First Out) 원칙을 따릅니다. 함수가 실행되면 콜스택 상단에 실행 컨텍스트가 추가(push)되고 함수 실행이 완료되면 콜스택에서 실행 컨텍스트가 제거(pop)되는 식으로 동작됩니다. 따라서 콜스택 최상단을 확인하여 현재 실행중인 함수를 확인할 수 있습니다.

자바스크립트는 단일 스레드 언어이므로 한번의 하나의 작업만 처리가 가능합니다. 즉 하나의 콜스택에 실행 컨텍스트가 추가되고 제거되며 동작합니다.

하나의 콜스택만 사용하여 동작한다면 매번 실행중이던 코드가 종료되길 기다린 후에야 새로운 코드를 실행할 수 있을것입니다. 이는 매우 비효율적입니다. 따라서 자바스크립트는 비동기 동작을 통해 이 비효율성을 해결하고 있습니다. 콜스택이 비어있을때 비동기 콜백이나 이벤트를 실행합니다.

🔗 조각조각 - 자바스크립트 이벤트 루프

2. 실행 컨텍스트 유형

실행 컨텍스트는 두가지 유형을 가지고 있습니다. 바로 전역 '실행 컨텍스트'와 '함수 실행 컨텍스트'입니다. 이는 컨텍스트의 생성 시기와 범위의 차이로 구분됩니다. 자바스크립트 코드가 실행될 때 처음 딱 한번 생성되는 컨텍스트를 '전역 실행 컨텍스트'라 합니다. 함수가 실행될 때 마다 새롭게 생성되는 컨텍스트는 '함수 실행 컨텍스트'라 합니다.

  • 전역 실행 컨텍스트(Global Execution Context)
    - 자바스크립트 코드가 실행 시 생성되는 기본 컨텍스트
    - 전역 스코프 관리 (전역 변수와 함수)
    - 스크립트 로딩 시 단 한 번만 생성됩니다.
    - 브라우저 환경에서는 window 객체가 전역 컨텍스트
    - Node.js에서는 global 객체가 전역 컨텍스트
    - 전역 컨텍스트는 모든 함수 컨텍스트의 상위 컨텍스트
    - 자바스크립트 동작이 모두 끝나야 콜스택에서 제거
  • 함수 실행 컨텍스트(Function Execution Context)
    - 함수가 호출될 때마다 새로운 함수 컨텍스트가 생성
    - 함수 실행이 완료되면 콜 스택에서 제거
    - 해당 함수의 지역 변수, 매개변수, this 값 등을 포함
    - 자신이 선언된 위치에 따라 외부 렉시컬 환경을 참조, 클로저 형성

전역 실행 컨텍스트와 함수 실행 컨텍스트의 특징을 정리해보자면 위와 같습니다.

콜스택과 실행 컨텍스트 예시

지금까지 실행 컨텍스트가 무엇인지와 전역 컨텍스트, 함수 컨텍스트를 알아보았습니다. 또 콜스택이 무엇인지 알아보았습니다. 이제 전역 컨텍스트, 함수 컨텍스트, 콜스택 동작 개념을 간단하게 정리해보도록하겠습니다.

callstack

왼쪽 콜스택 이미지는 자바스크립트 코드가 실행된 첫 모습입니다. 이 경우 자바스크립트 엔진이 콜스택에 전역 컨텍스트를 추가하게 됩니다.

가운데 콜스택 이미지는 함수 A가 실행된 모습입니다. 함수 A를 실행하기 위한 실행 컨텍스트, 즉 함수 A 컨텍스트가 콜스택에 추가됩니다. 이때 자바스크립트 코드가 모두 완료가 된것이 아니기 때문에 글로벌 컨텍스트는 여전히 아래 존재합니다.

오른쪽 콜스택 이미지를 보면 함수 A 컨텍스트가 여전히 글로벌 컨텍스트 위에 존재하고 이 위에 함수 B 컨텍스트가 추가되어 있습니다. 이는 함수 A가 종료되지 않은 상태에서 함수 B가 실행되었음을 뜻합니다. 즉 함수 B가 함수 A 내부에서 실행되었고 아직 함수 B가 완료되지 않아 콜스택 최상단에 남아있는 상태입니다. 만약 함수 A가 끝나고 함수 B가 순차적으로 실행되었다면 가운데 이미지에서 함수 A 컨텍스트가 제거되고 같은 자리에 함수 B 컨텍스트가 위치하고 있어야합니다.

3. 실행 컨텍스트 구성

앞서서 실행 컨텍스트의 유형을 알아보았습니다. 이번엔 실행 컨텍스트 내부는 어떤식으로 구성되어있는지 살펴보겠습니다. 실행 컨텍스트를 크게 세가지 구성요소를 가집니다.

  • 변수 환경(Variable Environment)
  • 어휘적 환경(Lexical Environment)
    - 환경 레코드
    - 외부 어휘적 환경 참조
  • This Binding
excutecontext

변수 환경

실행 컨텍스트가 생성될 때의 환경입니다. 이는 선언 시점의 렉시컬 환경의 복제값으로 일종의 스냅샷입니다. 따라서 어휘적 환경과 동일한 구조를 가지고 초기 변수와 초기 함수를 담고 있습니다. 하지만 var 타입 변수와 함수 선언문만이 저장됩니다.

어휘적 환경의 복제값인 스냅샷인데 왜 var 타입의 변수와 함수 선언문만이 저장되는지 의문이 듭니다. 왜 let, const 타입 변수나 함수 표현식은 저장이 되지않을까요?

답은 변수 환경 생성 시점에 있습니다. 이는 자바스크립트 엔진의 동작 순서와 실행 컨텍스트의 생명 주기를 더 알아야 이해하기 쉽습니다. 그렇기 때문에 아래에서 자세히 다루겠습니다.

기억해야할 것은 변수 환경은 어휘적 환경의 스냅샷으로 초기 변수와 함수가 저장되지만 var 타입 변수와 함수 선언문만 제한된다는 것입니다.

어휘적 환경(렉시컬 환경)

실행 중인 컨텍스트 내에서 변수와 함수 등 실행될때 필요한 정보가 모인 환경입니다. 변수 환경과 다르게 코드 실행 후 변경사항이 실시간으로 반영됩니다. 사실 어휘적 환경이 기본이고 초기 환경의 복제본을 남겨둔것이 변수 환경일 뿐입니다. 따라서 초기엔 변수 환경과 동일하지만 이후 변동 사항이 반영되어 변수 환경과 어휘적 환경은 서로 달라지게 됩니다.

환경 레코드

앞서 어휘적 환경을 실행 될때 필요한 정보가 모인 환경이라고 하였습니다. 환경 레코드는 이 중 변수와 함수가 관리되는 곳입니다. 이는 자료구조 형태를 띄며 식별자와 값의 바인딩을 관리합니다.

외부 어휘적 환경 참조

외부 어휘적 환경이란 함수가 선언된 시점의 스코프라고 할 수 있습니다. 이 외부 어휘적 환경에 접근하기 위한 참조값을 '외부 어휘적 환경 참조'라고합니다.

함수가 자신의 스코프에서 변수를 찾지 못한 경우 상위 스코프 즉 외부 어휘적 환경에서 변수가 존재하는지 확인합니다. 이는 변수를 찾을 때 까지 반복되며 이 과정을 스코프 체인이라고 합니다.

간단한 코드로 살펴보겠습니다.

var x = 10; // 전역 변수

function a() {
  var y = 20; // 지역 변수
  function b() {
    var z = 30; // 지역 변수
    console.log(x + y + z); // 60
  }
  b();
}

a();

여기서 b 함수는 a 함수의 내부에 선언 되었으므로, b 함수의 아우터 환경 참조는 a 함수의 스코프를 가리킵니다. 마찬가지로 a 함수의 아우터 환경 참조는 전역 스코프를 가리킵니다. 이는 함수의 선언 시점이 중요합니다.

outer

b 함수가 x, y, z 변수에 접근하려고 할 때, 다음과 같은 순서로 스코프 체인을 탐색합니다.

  1. b 함수의 스코프에서 z 변수 탐색.
    1. z 변수가 있으므로 그 값을 사용.
  2. b 함수의 스코프에서 y 변수 탐색.
    1. y 변수가 없으므로 아우터 환경 참조를 따라 a 함수의 스코프로 이동.
    2. a 함수의 스코프에서 y 변수 탐색. y 변수가 있으므로 그 값을 사용.
  3. b 함수의 스코프에서 x 변수 탐색.
    1. x 변수가 없으므로 아우터 환경 참조를 따라 a 함수의 스코프로 이동.
    2. a 함수의 스코프에서 x 변수 탐색.
    3. x 변수가 없으므로 다시 아우터 환경 참조를 따라 전역 스코프로 이동.
    4. 전역 스코프에서 x 변수 탐색. x 변수가 있으므로 그 값을 사용.
  4. x+y+z 결과 출력.

객체 환경 레코드, 선언적 환경 레코드

환경 레코드를 공부하다보니 let, const 타입 변수, 함수 표현식만 저장된다는 말을 보았습니다. 함수가 실행되며 필요한 환경(변수 및 함수)이 관리되는 곳이 환경 레코드인데 왜 var 타입 변수는 포함하지 않지? 함수 표현식은 또 왜 해당하지 않지? 머리가 아파오기 시작했습니다.

의문은 딥다이브 23장 실행 컨텍스트를 공부하며 풀렸습니다. 어휘적 환경의 구성을 '환경 레코드'와 '아우터 환경 참조'로만 단순하게 생각했기 때문입니다. 어휘적 환경은 전역 실행 컨텍스트에 포함된 것인지, 함수 실행 컨텍스트에 포함된 것인지에 따라 다른 모양을 합니다.

  • 전역 어휘적 환경

    • 환경 레코드
      • 객체 환경 레코드
      • 선언적 환경 레코드
    • 아우터 환경 참조
  • 함수 어휘적 환경

    • 환경 레코드
      • 함수 환경 레코드
    • 아우터 환경 참조

위에서 언급한 '환경 레코드에는 let, const 타입 변수 그리고 함수 표현식만 저장된다' 는 표현은 모든 환경 레코드에 해당되는 말이 아니였습니다. 바로 '선언적 환경 레코드' 의 설명이었습니다. 선언적 환경 레코드는 전역 실행 컨텍스트 환경 레코드의 구성 요소 중 하나입니다.

다시 말하자면 '환경 레코드'는 식별자와 식별자의 값으로 이루어진 자료구조로 변수와 함수가 관리되는 곳입니다. 굳이 변수 타입에 따라 구분하고 함수 선언식, 함수 표현식에 따라 구분지을 필요가 있을 까? 싶습니다. 그럼에도 전역 어휘적 환경의 환경 레코드에선 구분짓고 있습니다. 왜 그럴까요?

답은 자바스크립트의 var 타입 그리고 함수 선언문의 특징 때문입니다. var 타입 변수와 함수 선언문 코드는 전역객체(window, global)의 프로퍼티와 메소드가 되어 사용됩니다. 즉 전역 var 타입 변수와 전역 함수는 전역 객체가 되어 사용됩니다. 따라서 이를 환경 레코드에 저장할때 새롭게 식별자와 식별자 값을 바인딩하는 자료구조 테이블을 모두 만들어 저장하는 것 보단 해당 전역 객체의 참조값을 저장하는 것이 효율적입니다. 이 객체의 참조값을 저장하는 환경 레코드가 바로 '객체 환경 레코드' 입니다. 이후 남은 let, const, 함수 표현식을 자료 구조 형태로 관리하는 곳이 '선언적 환경 레코드' 입니다.

  • 객체 환경 레코드 : 전역 객체 참조값 저장
  • 선언적 환경 레코드 : let, const, 함수 표현식

함수 실행 컨텍스트 환경에선 위와 달라집니다. 함수 내부에서 선언된 var 타입 변수나 함수 선언식이 전역 객체의 프로퍼티와 메소드로 사용되지 않기 때문입니다. 이 경우 처음 생각했던 것과 같이 단순 자료 구조 형태의 함수 환경 레코드가 사용됩니다. 그렇기 때문에 함수 환경 레코드에서 var, const, let 타입 변수 모두 관리되며 함수 선언식, 함수 표현식 모두 관리됩니다.

전역 실행 컨텍스트, 함수 실행 컨텐스트의 어휘적 환경을 정리해보자면 아래 그림과 같습니다.

context

this 바인딩

함수가 호출될 때 결정되는 this의 값입니다. 실행 컨텍스트가 관련된 코드가 실행될 때 this 키워드가 참조하는 대상 객체입니다.

4. 자바스크립트 엔진 동작과 실행 컨텍스트 생명 주기

실행 컨텍스트가 무엇인지와 실행 컨텍스트 유형, 실행 컨텍스트 구성을 알아보았습니다. 이제 자바스크립트 코드가 실행되면서 실행 컨텍스트가 어떤 식으로 동작되는지를 살펴보겠습니다.

우선 자바스크립트 엔진은 코드를 실행할 때 크게 두단계, 평가와 실행 단계로 나눠 진행됩니다. 평가가 진행될 때는 코드의 선언문만 먼저 실행되 실행 컨텍스트가 생성되고 이후 실행 단계에서 선언문을 제외한 코드가 실행됩니다.

과정을 살펴보겠습니다.

  1. 자바스크립트 엔진 첫 실행
  2. 전역 코드 평가 (선언문)
  3. 전역 실행 컨텍스트 생성
  4. 전역 코드 실행 (선언문 제외 코드)
    • 전역 컨텍스트 환경이 사용되어 코드 실행
    • (실행 컨텍스트 생명 주기 - 실행 단계)
  5. 전역 코드 실행 중 함수 실행
  6. 해당 함수 코드 평가
  7. 해당 함수 컨텍스트 생성
  8. 해당 함수 코드 실행 (선언문 제외 코드)
    • 함수 컨텍스트 환경이 사용되어 코드 실행
    • (실행 컨텍스트 생명 주기 - 실행 단계)
  9. 5~8 과정 반복
  10. 전역 코드 실행 완료

🚨🚨🚨 자바스크립트 엔진 평가 단계

자바스크립트 코드를 스캔하고 준비하는 단계 실행 컨텍스트를 생성합니다. 실행 컨텍스트가 생성될때 환경 레코드에 식별자와 식별자 값이 등록됩니다. 이때 변수 타입과 함수 선언문, 함수 표현식에 따라 약간의 차이가 존재합니다.

  • var 타입 변수
    - 선언 : 변수 식별자 등록
    - 할당 : 식별자 값 undefined로 등록
  • let, const 타입 변수
    - 선언 : 변수 식별자 등록
    - 할당 : 할당 X (참조 불가능)
  • 함수 선언문
    - 선언 : 함수 식별자 등록
    - 할당 : 함수 코드 모두 등록 (호출 가능)
  • 함수 표현식
    - 선언 : 함수 식별자 등록
    - 할당 : 할당 X (참조 불가능)

모두 선언 과정을 거치며 식별자가 환경 레코드에 등록됩니다. 이로인해 코드가 아직 실행 단계를 거치기 전인데도 선언문들이 마치 해당하는 실행 컨텍스트 최상단에 존재하는 것과 같은 효과를 얻습니다. 이것을 바로 '호이스팅' 이라고 합니다. 호이스팅 관련 내용은 아래에서 자세히 살펴보겠습니다.

자바스크립트 엔진 실행 단계

생명 단계에서 수집된 정보를 바탕으로 코드가 실행됩니다. 평가단계에서 할당되지 않아 어휘적 환경에 아직 등록되어 있지 않던 let, const 타입 변수와 함수 표현식이 등록됩니다. 어휘적 환경은 변수와 함수의 변경을 실시간으로 반영하기 때문에 바로 바로 변경됩니다.

실행 컨텍스트 생성 단계

자바스크립트 엔진 평가 단계에서 선언문만 먼저 실행되어 실행에 필요한 환경, 실행 컨텍스트가 생성됩니다.

실행 컨텍스트 실행 단계

자바스크립트 엔진 실행 단계에서 실행 컨텍스트를 이용해 코드가 실행됩니다. (변수에 값이 할당되고 함수 호출)

정리하자면 자바스크립트 엔진 동작도 평가 단계와 실행 단계로 나뉘며 이 과정중 실행 컨텍스트가 생성되며 생명 단계와 실행 단계를 거치게 됩니다.

5. 🚨🚨🚨 변수 환경엔 왜 var 타입 변수와 함수 선언식만?

위에서 언급 하였듯 변수 환경에선 var 타입 변수와 함수 선언식만을 포함합니다. 변수 환경은 어휘적 환경의 스냅샷일 뿐인데 왜 어휘적 환경이 가진 모든 구성요소를 똑같이 가지는게 아니라 var 타입과 함수 선언문만 포함된다고 하는 건지... 정말 의문이었습니다.

의문은 변수 환경의 생성 시점을 알고나서 해결되었습니다. 변수 환경은 실행 컨텍스트가 생성된 직후 어휘적 환경의 초기 값이 복제된 것입니다. 복제 시점이 자바스크립트 엔진 평가 단계 후, 실행 단계 전이라면 변수 환경에 let, const 타입 변수와 함수 표현식이 포함되지 않는것이 이해됩니다.

자바스크립트 엔진의 평가 단계 후 var, let, const 변수, 함수 선언문, 함수 표현식 모두 식별자가 어휘적 환경에 등록됩니다. 하지만 let, const, 함수 표현식은 할당되지 않아 식별자만 어휘적 환경에 존재합니다. 이후 실행 단계에서 할당 코드가 실행되어야 값이 어휘적 환경에 등록됩니다. 따라서 실행 단계 전에 변수 환경이 복제 된것이라면 변수 환경에 var, 함수 선언문만 존재하는것이 납득됩니다. var 타입 변수는 undefined의 값을 가지고 함수 선언문은 함수 자체를 이미 가지고 있기 때문에 복제 가능하기 때문입니다.

그런데 여기서 추가 의문이 생겼습니다.

  1. 변수 환경은 변화하지 않는다. 즉, 변수 환경은 코드 실행 단계 이후와 이전이 동일하다.
  2. 그럼 변수 환경에 저장된 var 타입 변수는 다 undefined로 되어있는건가?
  3. 그럼 왜 필요하지?
  4. 변수 환경의 var는 무조건 undefined 값을 가지는 것이 아닌가?

네, 변수 환경에서 var 타입 변수가 undefined로 생성되고 변화하지 못해 쭉 undefined로 유지되는 것이 아니였습니다.

변수 환경은 변화하지 않는다. 즉, 변수 환경은 코드 실행 단계 이후와 이전이 동일하다.

위 말을 틀린 말이 아닙니다. 변수 환경은 실행 단계 이후와 이전이 동일합니다. 근데 이것이 var 타입 변수가 undefined를 그대로 가지고 있다는 뜻이 아니였습니다. 바로 변수 환경이 가리키는 메모리 주소가 같다는 것을 의미했습니다. 평가 단계에서 변수 환경의 var 타입 변수는 undefined 값을 할당 받습니다. 이때 특정 메모리 공간을 할당 받습니다. 이후 실행 단계에서 할당 코드가 실행되어도 새로운 메모리 공간을 할당받지 않고 기존의 메모리 공간을 계속 사용하여 값이 할당됩니다.

간단한 코드로 과정을 살펴보겠습니다.

var a = 10;
var b;
var c = a + b;

자바스크립트 평가 단계를 거치며 실행 컨텍스트가 생성됩니다. 실행 컨텍스트가 생성되며 어휘적 환경에 식별자와 바인딩된 값들이 등록됩니다. 이때 변수들이 var 타입이기 때문에 undefined 값을 할당 받으며 메모리 주소가 할당됩니다.

변수메모리 주소
aundefined0x0001
bundefined0x0002
cundefined0x0003

이후 자바 스크립트 실행 단계가 진행됩니다. a에 10이 할당되고 b는 아무 값도 할당 받지 않고 c는 a와 b의 합인 NaN가 할당됩니다. 이때 메모리 주소는 그대로입니다.

변수메모리 주소
a100x0001
bundefined0x0002
cNaN0x0003

지금까지의 내용을 정리하자면 변수 환경이 let, const, 함수 표현식을 가지지 않는 이유는 변수 환경이 생성되는 시점에 let, const, 함수 표현식의 값이 할당되지 않은 상태이기 때문입니다. 또 실행 단계 이전과 이후가 같다는 것은 변수 환경이 가리키는 메모리 주소가 같다는 것을 뜻하고 그렇기 때문에 변수 환경에 저장된 var 타입 변수의 값은 실행 단계에서 할당되어 변경 가능합니다. 즉, 동일한 메모리 주소를 가지고 있으므로 이 할당을 보고 변수 환경이 변경되었다고 하지 않습니다.

참고 자료

profile
프론트엔드 개발자

0개의 댓글