Execution Context와 Call Stack [JavaScript Engine]

Dengo·2023년 9월 13일
0

JavaScript

목록 보기
6/6
post-thumbnail

웹 브라우저에서의 JavaScript 코드 실행되기 까지
지난번에 작성했던 JavaScript코드가 엔진에서 어떤 과정을 거쳐서 실행되기에 이르는지를 봤다면,
이번 글에서는 실행에 초점을 두어 살펴보도록 하겠습니다.

이번 주제를 비롯해서 실행과 관련된 JavaScript의 내용은 꽤나 방대할 것 입니다.
JavaScript의 코드가 실행되면서
식별자에 대한 정보의 관리,
스코프와 스코프 체이닝 그리고 클로저란 무엇을 의미하는지,
비동기 상황에 대해서는 어떻게 동작하는지
대략적으로 생각해봐도 다뤄야 하는 주제가 다양할 수 밖에 없습니다.

따라서 여러번에 나누어 내용을 정리할 것이며
이 모든 주제의 근본적인 출발점인 Execution Context를 먼저 알아보도록 하겠습니다.

Execution Context 알아보기

Execution Context란 JavaScript에서 코드가 실행되는 환경을 의미합니다.
조금 더 쉽게 표현을 해보자면 '실행할 코드에 제공할 정보들을 모아놓은 객체'
라고도 표현할 수 있겠습니다.

여기서 말하는 '제공할 정보들'이란 변수, 함수, Scope, this와 같은 정보를 의미합니다.

이러한 정보를 담고있는 Execution Context는 두 가지 타입으로 나눌 수 있습니다.

  • Global Execution Context
  • Function Execution Context

하나씩 살펴볼까요?

Global Execution Context (전역 실행 컨텍스트)


우리 모두가 알다시피 위의 코드는 정상적으로 동작할 수 있습니다.
아주 기본적인 사실이죠.

하지만 이것을 가능케 하기 위해선 위에 설명했듯이
'코드가 실행되는 환경' 즉, Execution Context가 필요하고,
위 코드와 같이 전역 환경에서 실행되는 코드를 위해서 전역 환경의 Execution Context가 최초에 만들어지게 됩니다.

이 과정에서 a, b 값이 이 Global Execution Context에 저장되고
이 실행 컨텍스트의 this는 전역객체를 가리키게 됩니다.

  • 웹 브라우저 -> Window 객체
  • Node환경 -> Global 객체

참고로 엄격 모드(Strict Mode)에서 실행시에는
Global Execution Context의 this에 undefined가 저장됩니다.

Function Execution Context (함수 실행 컨텍스트)

전역에서 코드가 한 줄씩 실행되면서 함수를 호출하게 될 것 입니다.
JavaScript 엔진은 이런 함수들이 실행될 때 이 함수의 실행을 위한 별도의 환경을 만들게 되는데 이 환경을 Function Execution Context라고 부릅니다.

즉 정리를 하자면 코드가 실행될 때 최초 1번 Global Execution Context, 함수가 호출될 때 마다 Function Execution Context가 생성됩니다.

Call Stack

여기서 우리가 가져야 하는 의문점은
"왜 이러한 환경이(실행 컨텍스트) 필요한걸까?"
를 다시 생각해 보아야 합니다.

그리고 이것에 대한 간단한 대답으로
자신의 범위(Scope)내 에서 정보를 기억하기 위해서
라고 할 수 있겠죠.

여기서 추가적으로 살펴봐야 하는 상황이 있습니다.

이 코드에서는 크게 두 가지 중요한 내용을 보여주고 있습니다.
1. 함수 b에서 자신의 상위 Scope에 있는 변수 a를 참조 가능.
2. 함수 b에서 선언된 변수 x는 b의 실행이 종료되면 메모리에서 사라짐.

간단한 예시를 들었음에도 단순히 '환경을 준비한다'만으로는 이 두 가지 문제가 해결될 수 없습니다.
정리하자면 다음의 두 가지 문제를 해결해야합니다.

  • Execution Context가 생성되는 시점에 자신의 상위 Scope을 바로 알 수 있어야 하고, 참조가 이루어져야 함.
  • 함수의 실행이 모두 끝나고 나면 Execution Context가 필요 없어짐

JavaScript 엔진에서는 이런 문제를 한방에 해결하기 위하여 Execution Context를 저장하는 자료구조로 Stack을 선택했습니다.
이 Stack을 JavaScript 엔진에서는 Call Stack이라고 부릅니다.

Call Stack 동작 예시



글자가 좀 작지만,, 그림을 통해서 Call Stack이 Execution Context(이하 EC)를 관리하는 모습을 한눈에 볼 수 있습니다.

중요한 부분만 그림을 통해서 다시 볼까요?

  • 함수 one의 EC가 스택에 올라갈 때 오로지 스택의 최상단(top) 인 Global EC만을 참조하면 됩니다.
  • console.log('1')역시 함수의 실행이기에 EC가 생깁니다. 따라서 스택에 추가되고 1을 출력하고 금방 스택에서 제거(pop)됩니다.
  • 이후에는 함수 two역시 추가되었다가 실행이 전부 완료된 뒤에 제거(pop)됩니다.
  • 스택을 자료구조로 채택함으로써 현재 실행 중인 함수를 스택의 최상단(top)으로 바로 알아낼 수 있으며 EC의 제거 역시 스택의 최상단이 없어지면 되게끔 하였습니다.
  • 마지막에는 Global EC까지 스택에서 없어진 모습입니다.

왜 Stack 이어야 했을까요? (참고)

스택의 가장 큰 특징으로 후입 선출 구조 (Last In First Out)를 흔히 꼽곤 합니다.
조금 더 명확하게 하자면 저는 스택 혹은 큐가 갖는 의의로 '시간'에 의미 부여를 하고 싶습니다.
사용자가 데이터들을 저장하고 이것에 접근하는 기준을 두는 것이죠.

가령, 스택을 사용할 경우
"이 데이터는 가장 최근에 추가한 데이터만 조회할 수 있어"
이것은 언뜻 제약으로 보입니다만, 조금 달리보면

"이 데이터는 가장 최근에 추가된 데이터만 조회하면 돼"
라는 일종의 탐욕법으로
"따라서 가장 최근에 저장된 데이터만 신경쓰면 돼"
가 되는 것이죠.

원하는 데이터를 찾기 위한 시간 복잡도가 O(1)이 되는 엄청난 시간적 이익을 갖게 됩니다.
그러니까 어차피 가장 최근에 추가된 데이터만 필요할거면 스택을 쓰는게 완전 이득인거죠.

우리가 이미 살펴보긴 했지만 결국 JavaScript엔진에서도 EC에 대해서 '가장 최상단의 EC'는 곧 '현재 실행 중인 EC'를 의미 합니다.

  • 실행이 끝났을때 없어져야 하는 EC
  • 새로 추가된 EC가 참조해야 하는 EC

둘 다 '현재 실행 중인 EC (즉 가장 최상단의 EC)'에 해당하기 때문에 Stack을 사용해야 했다고 정리할 수 있겠네요

Identifier Resolution (혹은 Scope Chain)

위의 코드 예시에서도 two함수는 console.log를 자연스럽게 사용하고 있습니다.
또 가령 two함수에서 Array나 Object같은 전역 객체 소속의 함수를 당연히 사용할 수 있다는 것을 우리는 모두 알고 있습니다.

그런데 two함수의 EC는 오로지 one함수의 EC만을 바라보고 있습니다.
전역 객체는 Global EC의 것인데 말이죠.

사실 굳이 이야기할 필요가 없을 수 있겠지만,
one함수의 EC가 Global EC를 참조하고 있을 것 이니 쉬운 표현으로 '건너 건너' 사용 가능합니다.
(console.log를 two에서 찾음, 없음 -> 건너서 one에서 찾음, 없음 -> 건너서 Global EC에서 찾음, 있음!!)

조금 더 어려운 표현으로
EC의 실행 관리는 Stack을 통해서 이루어지지만
식별자 참조 관리는 단방향 Linked List의 형태를 하고 있다는 것을 말씀드리고 싶습니다.

Linked List의 특성상 맨 처음 노드를 HEAD로 두고 원하는 정보(지금 예시에선 console, Array, Object와 같은 식별자)를 순차적으로 찾는 것이죠.

여기에서도 역시 맨 처음 노드를 결정하는 것은 Stack이라는 자료구조 안에서 꽤 결정하기 쉽겠네요.

이것을 Identifier Resolution (혹은 Scope Chain)이라고 부릅니다.

~참고~
Scope Chain: 식별자를 찾는 매커니즘
Prototype Chain: 프로퍼티 혹은 메서드를 찾는 메커니즘
(따라서 둘 다 Linked List의 자료구조를 취하고 있다. ㅇㅇ)

이 참조에 대한 주제는 사실 다음 주제인 Lexical Environment에서 구체적으로 알아볼 예정입니다.
그치만 이 정도는 이야기 하고 넘어가야 속 시원하잖아요

Execution Context 생성 과정

함수가 호출(실행) 될 때 EC가 만들어진다고 앞서 살펴봤습니다.

  • Creation Phase
  • Execution Phase

이 두 가지 과정을 차례로 거쳐 만들어지는데, 하나씩 살펴보도록 하겠습니다.

Creation Phase (생성 단계)

arguments객체, 변수, 함수 선언 초기화

  1. Global Execution Context를 제외하고 모든 Function Execution Context는 arguments객체를 갖는데 생성 단계에서 세팅됩니다.

  2. 함수 안에 선언된 변수의 경우 이때 Execution Context에 등록됩니다.
    등록되고 나서 이 변수에 일단은 undefined 값을 할당시킵니다.
    생성 단계에서 준비를 위해 변수를 먼저 끌어올려서 세팅해 놓는 현상을 두고 변수 호이스팅이라고 부릅니다.

  3. 함수 선언문(함수 표현식과 혼동X)의 경우에도 함수 호이스팅이 일어납니다.
    변수와는 다르게 함수 객체 자체가 생성 단계에서 저장됩니다.
    즉 함수 선언문에 한해서 선언 이전에 해당 함수를 실행해도 문제없이 실행 가능한 이유가 이 때문입니다.

그리고 생성 되었기 때문에 이때 Call Stack에 push가 일어납니다

Scope Chain 생성 후 초기화

역시 다음 챕터(Lexical Environment)에서 더 자세히 보겠지만 만들어지고 있는 Execution Context의 상위 환경을 참조하게 됩니다.

이 과정이 있기 때문에 Scope Chaining으로 식별자 탐색이 가능해집니다.

this binding

과거에도 this binding을 주제로 작성한 글이 있습니다.
해당 글에서도 언급한 함수 호출 패턴에 따라서 this가 참조하는 대상이 결정됩니다.

Execution Phase (실행 단계)

실행 단계에서 일어나는 일

말 그대로 우리가 예상하는 JavaScript 코드가 실행되는 단계입니다.

  • 변수에 저장된 값
  • 변수에 저장된 함수 표현식

이 앞서 생성 단계에서 undefined가 일단 저장된다고 말씀드렸는데
실행 단계에서 본래의 값을 저장하게 됩니다.

함수의 실행이 전부 종료되면 (실행단계가 끝나면) Call Stack에서 pop이 일어납니다.

호이스팅으로 인한 참조 가능 예시


이 코드에서 console.log를 실행하는 시점은 말그대로 실행 단계입니다.
각각의 결과가 undefined, "world"로 나오는 이유가 앞서 생성 단계에서 호이스팅되었기 때문임을 알 수 있습니다.

let과 const선언의 경우에도 호이스팅이 될까요? (참고)

네! 물론 호이스팅이 var선언과 동일하게 일어납니다.
그러니까 생성 단계에서 undefined로 동일하게 저장될 것 입니다.

다만, 변수 호이스팅의 경우 Execution Context의 변수를 말 그대로 준비해놓기 위함이지 위의 var를 사용한 예시처럼 undefined값을 조회 가능하게 하는 것은 예측 불가능한 범위로 보는게 맞을 겁니다.

따라서 이후에 만들어진 let, const의 경우 이 문제를 시정하고자 초기화 이전에 참조하지 말라는 에러가 나오게 됩니다.

Call Stack 다시 보기


다시 한번 복습차 보기 위해 예제를 가져왔습니다.
1. Call Stack에 a추가
2. Call Stack에 console.log추가
3. Call Stack에 b추가
4. Call Stack에 b제거
5. Call Stack에 console.log제거
6. Call Stack에 a제거

다시 보는 JavaScript엔진 동작과정

  1. JavaScript 코드 파싱
  2. Abstract Syntax Tree (AST) 생성
  3. 전역 실행 컨텍스트 생성
  4. 전역 실행 컨텍스트 실행
  5. 함수가 실행된다면 그 함수의 실행 컨텍스트 생성, 실행
  6. 전역 실행 컨텍스트가 종료될때까지 반복

마무리

글이 다소 길어졌지만 JavaScript 코드가 실행되는 가장 기본적인 환경을 알아보았습니다.
다음 주제로는 Execution Context 내부를 더 자세하게 보기 위해 Lexical Environment를 알아봐야 하고 동시에 JavaScript에서 자주 언급되는 Closure를 알아봐야겠습니다.

긴 글 읽어주셔서 감사하고 틀린 부분이 있다면 지적 부탁드립니다!🙇‍♂️

profile
Software Engineer (전산쟁이)

0개의 댓글