[JavaScript] JS의 동작원리 - 1 (Execution Context)

coderH·2022년 4월 4일
1

JavaScript 연대기

목록 보기
7/11
post-thumbnail

JS의 동작원리 1편 (Exeuction Context)

오늘 다뤄볼 주제는 JavaScript의 핵심이라고 할 수 있는 Execution Context에 대해서 다뤄보려고 합니다.

EC라고도 불리는 이 개념에 대해서 이해를 하게 되면 호이스팅, this, TDZ, 스코프, 클로저 등 JS에서 다루는 대부분의 개념과 동작 원리에 대해서도 함께 이해할 수 있습니다.

추상적인 개념이고 생소한 단어가 많이 나와서 한 번에 이해하기 어려울 수 있지만
중요한 부분인 만큼 도형 자료와 함께 최대한 이해하기 쉽도록 이야기해보겠습니다.

Execution Context 란?

Exeuction Context는 EC 또는 실행 컨텍스트라고도 불리며 ES5부터 생긴 개념입니다.
EC는 코드가 원활하게 실행될 수 있도록 코드를 읽기 전, 미리 실행환경을 만들어주는 역할을 합니다.

우리가 코드를 작성할 때 함수, 모듈, 패키지로 나누는 것처럼
JS엔진도 코드의 복잡성을 줄이고 관리하기 쉽도록 실행환경을 미리 만들게 됩니다.

EC는 우리가 만든 코드의 1번 라인을 읽기 전,
특정 범위에서 선언된 모든 변수와 함수들의 존재에 대해서 JS엔진이 알 수 있도록 저장하고 있습니다.

위 이미지는 아직 JS엔진이 코드를 읽기 전 상황이며 오른쪽 화면에서 EC내부에 전역변수로 선언된 atest함수의 이름과 초기화가 진행된 모습이 보입니다.


EC의 종류

EC는 3개의 종류로 나뉘어 있으며 이들을 통칭하여 EC라고 부르고 있습니다.
EC의 종류와 특징에 대해서 알아보겠습니다.

Global Execution Context (GEC)

JS 실행 시 가장 먼저 생성되는 EC로서 전역 범위를 담당합니다.
GEC는 프로그램의 크기와 상관없이 프로그램 내부에 오직 하나만 존재할 수 있습니다.

또한 코드가 한 줄도 작성되어있지 않아도 JS 파일을 실행하는 것만으로 생성되기 때문에 코드에 상관없이 실행만으로도 생성되는 특징을 가집니다.

스크립트 내의 모든 코드가 실행되고 나면 GEC는 삭제되면서 프로그램이 종료됩니다.

Functional Execution Context (FEC)

스코프에 대해서 알고 계신다면 JS에서 함수는 자신만의 스코프를 가진다는 것을 알고 계실 겁니다.
함수의 스코프처럼 FEC는 함수의 선언이 아닌 호출 시에 생성됩니다.

그래서 프로그램 실행 시 먼저 GEC가 만들어진 후 코드를 읽다가 함수 호출을 만나게 되면 그때 FEC가 생성됩니다.
이후 함수 내부의 모든 코드를 순차적으로 실행하며 해당 함수 블록이 끝나게 되면 FEC는 삭제됩니다.

// GEC 생성완료

let a = 10;

function test() {
	console.log("test!");
} // FEC 삭제

test(); // FEC 생성

// GEC 삭제

Eval Execution Context (EEC)

EEC는 JS의 내장함수 중 하나인 Eval메소드를 호출했을 때 생성되는 실행 컨텍스트입니다.

Eval메소드는 인자로 받은 string을 JS코드로 실행시키는 함수이며 보안에 취약해질 수 있어 거의 사용되지 않습니다.

EEC는 strict mode에서만 자신의 지역 범위를 가지며
만약 'use strict'를 명시하지 않았다면 Eval메소드의 this는 전역 객체를 가리킵니다.

"use strict";

let x = 17;
let evalX = eval("let x = 42; x");
console.log(x === 17);
console.log(evalX === 42); 

위 코드는 strict mode로 실행되었기 때문에 evalX 변수는 자신만의 범위를 가지게 되고
자신의 지역 범위에서 생성된 지역변수x의 값 42를 반환하게 됩니다.


Execution stack

위에서 EC가 생성되고 삭제된다고 표현했었습니다.
EC는 Execution stack이라고 불리는 곳에 저장되는데요.

이름 그대로 stack구조이기 때문에 LIFO(Last In First Out)형태로 데이터가 생성 및 삭제되며
따라서 EC가 생성될 때마다 Execution stack의 상단에 추가되고 해당 EC 내부의 코드가 모두 실행되고 나면 Execution stack에서 삭제되며 JS엔진은 아래에 있던 EC를 마저 실행합니다.

위와 같은 코드에서 EC의 생성과 삭제에 따른 Execution stack의 변화는 아래와 같습니다.

Execution stack은 Call stack과 같은 개념이며 다른 프로그래밍 언어에서는
program stack, control stack, run-time stack, machine stack으로 불리기도 합니다.

JS는 싱글쓰레드이기 때문에 한 번에 하나의 일 처리밖에 하지 못합니다.

따라서 위 그림에서 GEC실행 중 test1 함수가 호출되면 GEC 실행을 잠시 멈추고
test1의 FEC를 생성 및 실행하며 마찬가지로 test2의 호출을 만나게 되면
test1 FEC를 잠시 멈추고 test2의 FEC를 먼저 생성 및 실행하게 됩니다.

만약 Execution stack이 없다면 JS엔진은 어떤 부분을 실행시켜야 하는지,
해당 범위의 실행이 끝나면 어떤 코드를 실행해야 하는지 알 수 없으므로
JS엔진은 Execution stack을 참조하며 실행해 나갑니다.

그래서 Execution stack은 EC를 저장하고 JS엔진이 어떤 것부터 실행해야 하는지 순서를 알려주는 역할을 한다고 할 수 있습니다.


EC의 2가지 단계

EC는 생성단계와 실행단계라는 2개의 단계를 거치며 생성됩니다.

생성단계 (Creation Phase)

생성단계는 지금까지 위에서 설명한 것처럼 EC가 자신의 범위내에 있는 코드를 읽기 전에 발생하며
생성단계에서 이루어지는 작업들은 아래와 같습니다.

  • EC를 위한 객체 생성 ( GEC는 전역객체, FEC는 함수의 인자를 가진 arguments객체 )
  • this 객체를 생성하며 위에서 생성된 객체를 바인딩
  • 현재 EC의 범위 내에 선언된 변수와 함수에 대한 참조를 메모리힙(변수와 함수들을 저장하는 장소)에 저장
  • 메모리힙에 저장된 변수 중 var로 선언된 변수와 함수들에 한해서 초기화 과정(undefined값) 진행

위에서 bold처리가 된 두 부분만 조금 더 자세하게 다뤄보겠습니다.

1. FEC는 함수의 인자를 위한 arguments객체를 생성한다.
함수는 자신만의 arguments객체를 가지고 있습니다.
이는 FEC의 생성과정에서 만들어지며 arguments객체 내부에는 우리가 인자로 받은 변수들의 값이 담겨있습니다.
따라서 우리가 매개변수명을 정의하지 않아도 arguments[index]형태만으로 인자에 접근할 수 있습니다.

또한 위 사진에 보이는것처럼 arguments 객체는 유사배열 형태로 제공됩니다.

유사배열 : 배열처럼 대괄호에 감싸져 있으며 인덱스로 요소에 접근이 가능하고 length라는 속성을 가지고 있지만 실제로는 array가 아닌 object이며 따라서 배열의 메소드를 사용할 수 없는 객체
Ex. 함수의 arguments객체, querySelectorAll과 같은 메소드로 다수의 DOM node를 리턴한 객체

또한 사실 우리가 매개변수명을 정의한다는것은 인자로 받은 값들을 FEC내의 지역변수로 선언하는 것입니다.

그래서 매개변수명을 통해 인자로 받은 값을 변경해보면 아래 코드처럼 함수 내에서는 값이 변경됩니다.

let x = 3;

function test(para) {
	console.log(para); // 3
    para = 10;
    console.log(para); // 10
}

test(x);

console.log(x); // 3

para변수는 test라는 함수의 지역변수이며 지역변수의 값을 변경했기 때문에 외부에 있는 x의 값은 변경되지 않는것입니다.

하지만 반대로 아래 코드처럼 외부 변수를 인자로 받지 않고 참조하여 값을 변경할 경우 변경된 값이 할당되는것을 볼 수 있습니다.

let x = 32;

function test() {
  x = 11;
  console.log(x); // 11
}

test();

console.log(x); // 11
  1. var로 선언된 변수와 함수들에 한해서 초기화 과정을 진행한다.

이전에 호이스팅의 원리와 TDZ가 발생하는 원인 이라는 글에서도 다뤘었지만
변수의 생성과정은 총 3단계로 이루어지며 변수의 선언은 EC내에 변수명을 등록만 하는 과정이고
초기화 과정은 선언의 다음 단계로 undefined라는 값을 할당하여 변수에 접근이 가능한 상태입니다.

EC의 생성과정에서 letconst로 선언된 변수는 선언단계까지,
var로 선언된 변수와 모든 함수는 초기화 과정까지 이루어집니다.

그래서 우리가 호이스팅된 변수에 접근을 하면 undefined라는 값을 반환하는 경우가 이러한 이유 때문이며 let, const로 정의한 변수들은 변수선언만 이루어진 상태이므로 접근이 불가하여
"초기화 이전에는 접근할 수 없다"는 에러를 반환합니다.


실행단계 (Execution Phase)

실행단계는 작성된 코드를 읽고 실행하는 단계로 변수에 실제 값을 할당하거나 함수를 호출합니다.
이 과정은 한줄한줄 읽어가면서 순서대로 진행됩니다.

생성과정에서 선언만 이루어지는 let, const 변수는 코드상의 변수 선언문을 만나기전까지는
사용하지 못하는 상태이므로 일시적 사각지대(TDZ)가 발생하게 됩니다.

변수bc는 선언문을 만나기 전까지 오른쪽의 상태처럼 값이 비어있는 상태가 됩니다.


마지막으로 EC를 이해하는데 도움이 될 만한 사이트를 소개해드리려고합니다.

링크 : JavaScript-Visualizer

JavaScriptVisualizer라는 사이트이며 왼쪽 창에 작성한 코드를 토대로
오른쪽 창에서 JS가 실행될 때 EC가 어떻게 변하는지 시각적으로 확인할 수 있는 사이트입니다.

아직은 ES5기준으로만 코드 작성이 가능해서 letconst는 지원되지 않지만
var키워드만으로도 충분히 흐름은 파악할 수 있으므로 한 번 사용해보시는걸 추천해 드립니다.

참고로 Step버튼은 클릭시마다 한 단계씩 JS가 실행해나가는 모습을 볼 수 있고
Run, Pause, Restart를 통해서 실행을 제어할 수 있습니다.
또한 Run speed바를 통해서 실행하는 속도도 조절할 수 있습니다.


2편에서는 Execution Context의 내부 구조에 대해서 다뤄볼 예정입니다.

요약

  • EC는 JS가 원활하게 실행되도록 코드실행 전 실행환경을 만들어주는 역할을 한다.
  • EC는 GEC, FEC, EEC 3종류가 존재한다.
  • Execution Stack은 call stack과 같은 개념으로 EC를 저장하는 역할을 하며 JS엔진은 이를 참고하여 순서대로 작업을 진행한다.
  • EC는 생성단계와 실행단계 2개의 과정을 거친다.
    • 생성단계에서는 JS엔진이 내부 코드를 읽기 전 EC를 생성하며
      이 과정으로 인해 호이스팅과 TDZ가 발생한다.
    • 실행단계는 작성된 코드를 한줄한줄 읽고 실행해나가는 단계이다.

참고 사이트

Joon Sik Yang | Medium

ui.dev

0개의 댓글