[JavaScript] JS동작원리 - 2 (EC의 내부구조)

coderH·2022년 4월 18일
1

JavaScript 연대기

목록 보기
8/11
post-thumbnail

JS 동작원리 2편 - EC의 내부구조

오늘은 이전편에서 다뤘던 EC의 내부구조에 대해서 알아보고
스코프 체인, this, 클로저까지 함께 다뤄보려고 합니다.

EC의 내부구조가 생각보다 복잡하기 때문에 이해가 쉽도록 도형과 함께 이야기해보겠습니다.

EC의 2가지 컴포넌트

지난편에서도 말했듯이 EC는 생성단계와 실행단계라는 2개의 단계를 거치면서 생성됩니다.

EC의 내부구조는 EC의 생성단계에서 구성되며
Execution Context는 Lexical Environment(LE)Variable Environment(VE)라는 2개의 컴포넌트를 생성합니다.

주의!
ES5까지는this값을 결정하고 저장하는 역할을 하는 ThisBinding컴포넌트를 포함하여
3개의 컴포넌트가 생성되었으나 ES6부터는 이를 제외한 LE와 VE만 생성됩니다.

ThisBinding컴포넌트의 역할을 대신 담당하는 메소드들이 아래에서 다룰
Declarative Environment Record라는 컴포넌트 내부에 추가되었습니다.


Lexical Environment (LE)

Lexical Environment는 어휘적 환경, 어휘환경 이라고도 불리며
EC내에서 코드에 의해 만들어진 변수와 함수들의 식별자를 속성으로 가지는 객체입니다.

즉, 함수와 변수들을 저장하는 역할을 합니다.

식별자란 변수와 함수의 이름을 뜻합니다.
따라서 식별자를 가진다는것은 변수와 함수에 대한 이름을 저장하는 것입니다.

Variable Environment (VE)

VE는 LE의 한 종류로 LE와 같은 구조를 가지고 있습니다.

ES6부터 let, const 키워드가 추가되면서 VE는 오직 var키워드를 사용하여 변수를 선언할 때만 사용됩니다.

따라서 EC는 생성 시 아래와 같은 구조를 가지게 됩니다.

Execution context: {
	Lexical Environment: {},
    Variable Environment: {},
}

LE와 VE또한 2개의 컴포넌트를 생성하는데요.
Environment RecordThe outer environment reference입니다.

LE와 VE의 구조가 같기 때문에 아래부터는 편의상 LE로 통칭하겠습니다.


- Environment Record (envRec)

지역변수와 this의 값을 저장하는 곳이며
여기에서도 추가적으로 2개의 컴포넌트가 생성됩니다.

1. Declarative Environment Record

이 곳은 함수와 변수의 선언뿐만 아니라 Try구문의 catch와 같은 요소에 대해 식별자와 연결하여 정의하고 저장하는 역할을 합니다.

const, let, class, module, import, function의 키워드들이 모두 포함됩니다.

ES5의 ThisBinding 컴포넌트가 하던 역할을 이 컴포넌트에서 넘겨 받았습니다.
이 컴포넌트의 BindThisValue라는 메소드를 통해 this의 값을 초기화하고 저장합니다.

2. Object Environment Record

이 컴포넌트는 with구문 사용시 생성되는 바인딩 객체와 연결됩니다.

  • with 이란?

강제로 scope를 확장할 때 사용되는 구문으로
with(expression) {statement} 형태로 사용합니다.

예를 들어, expression을 Math로 줄 경우 기존 this의 scope에 Math를 포함시킴으로써
Math를 명시하지 않고 사용할 수 있도록 합니다.

with(Math) {
	sqrt(4); // 2
}

다만, 이 with구문은 일반적으로 예측가능한 scope의 범위를 임의로 조정하는 것이기 때문에 협업시에 혼란을 야기할 수 있어 권장되지 않습니다.


- The outer environment reference (outer)

이 컴포넌트는 함수의 외부 LE를 참조하는 컴포넌트입니다.

이를 통해 우리는 함수의 외부 범위에 접근이 가능하며 스코프체인, 클로저등이
이 컴포넌트를 통해 발생할 수 있습니다.

함수가 중첩되어있지 않을 경우 outer는 전역객체를 가리키며
중첩된 함수의 내부함수에서는 자신의 외부 함수 LE를 가리키게 됩니다.

function foo() {
	function bar() {
    	// bar의 outer는 foo의 LE를 가리킵니다.
    }
    // foo의 outer는 전역객체의 LE를 가리킵니다.
}

각 함수의 LE는 외부LE에 대한 평가를 기반으로 한 LE를 outer로 가지게 됩니다.
여기서 평가라는것은 함수의 인자를 받은 상태를 말합니다.

function foo(a) {
    return function (b) {
        return a + b;
    }
}

const three = foo(3);
console.log(three(5)); // 8

위와 같은 코드에서 three라는 변수의 값으로 foo함수와 함께 3을 인자로 전달했습니다.
이렇게 되면 three변수에는 3의 인자가 전달된 foo함수의 환경이 저장됩니다.
따라서 three(5) 형태로 함수 호출을 할 경우 저장된 a = 3과 인자로 전달된 5가 더해진 8이라는 값이 반환되게 됩니다.


스코프 체인

JS에서 변수를 호출하면 먼저 자신의 지역범위에 있는지 확인하고
없다면 위에서 설명한 자신의 outer가 참조하는 외부 LE에 접근하여 찾습니다.

변수를 찾을 때까지 외부 LE에 접근하여 찾는 과정을 반복하다가 GEC에서도 해당 변수를 찾지 못하면
그 때 정의되지 않은 변수를 호출했다는 에러가 발생하게 됩니다.
정확히는 GEC의 outernull이기 때문에 null을 만나게되면 체이닝 과정을 멈추게 됩니다.

따라서 스코프 체인이란 자신의 지역 범위에서 선언되지 않은 변수를 외부에서 찾아가는 과정이라고 할 수 있습니다.

let a = 3;

function foo() {
    let b = 5;

    function bar() {
        console.log(a); // 3
    }
    bar();
}

foo();

위와 같은 코드에서 bar함수가 a라는 변수를 찾아가는 과정은 아래 도형과 같습니다.

  1. bar의 EC 내부에서 변수a를 찾아본다.
  2. bar내부에는 없는 변수이므로 outer를 통해 foo의 EC에 접근하여 a를 찾는다.
  3. foo에도 없으므로 foo의 outer인 GEC에 접근하여 a를 찾는다.

클로저 (Closure)

사실 MDN에서 클로저의 정의를 보면 "함수와 함수가 선언된 어휘적 환경의 조합"이라고 설명되어 있어서 무슨 말인지 잘 와닿지 않았습니다.

좀 더 쉽게 풀어보자면 클로저는 함수 내부에서 외부범위에 선언된 변수에 접근하는것을 클로저라고 합니다.

let a = 3;

function test() {
	return a;
}

test();

위 코드에서 test함수는 자신의 외부에서 선언된 변수 a를 참조하기 때문에 test함수를 클로저함수라고 합니다.

JS의 경우 이 두 함수에 대해 구분이 없어 모든 함수는 클로저함수가 될 수 있습니다.
하지만 다른 프로그래밍언어에서는 클로저함수로 사용하기 위해서 함수 선언시 별도의 키워드를 붙여 일반 함수와 구분해야 하는 경우도 있습니다.

그래서 JS외에 다른 프로그래밍언어를 경험해보지 않았다면 외부 변수를 참조하는게 당연하다고 생각되어 함수와 클로저함수를 나눈다는 의미가 잘 와닿지 않을 수 있습니다.

그렇다면 MDN에서는 왜 그렇게 복잡하게 설명을 해놓았을까요?

그건 아마도 반환되는 함수는 자신의 LE를 기억하고 있다는 것을 중점으로 설명하고자해서 그런게 아닌가 싶습니다.

이전 편에서도 말씀드렸듯이 EC는 자신의 함수 내부에 있는 코드를 모두 실행하고 나면
Execution stack에서 삭제됩니다.

let b = 10;

function outer() {
    let a = 3;

    return function() {
        return a;
    }
}
const test = outer();

하지만 위와 같이 중첩된 함수 구조에서 함수를 반환하는 방식으로 코드를 작성하면
test변수에는 반환된 함수의 LE가 기억된 상태로 할당됩니다.

따라서 test변수는 반환되는 함수의 a의 값은 3이다라는 것을 알 수 있습니다.

혹시 클로저에 대한 설명이 더 필요하신분은 굉장히 쉽게 설명해주는 영상이 있어서 아래에 링크를 첨부해두었습니다.

Taehoon | Youtube


여기까지 보면서 스코프와 비슷한데? 스코프와 같은거 아닌가? 라는 생각이 드신 분들이 있으실텐데요

두 개념은 매우 비슷하지만 차이점이라면 LE는 실행 전에 코드 실행 환경을 만들기 위해 생성되는 추상적 개념이며 우리가 직접 접근할 수 없습니다.
반면 스코프는 VE를 포함하여 코드 실행시 변수와 함수를 저장하는 역할로 추상적 개념이 아니기 때문에 접근이 가능합니다.

마지막으로 오늘 다룬 EC의 내부구조를 객체로 표현하면 아래와 같습니다.

Execution Context = {
	Lexical Environment: {
    	Environment Record: {
        	Declarative Environment Record: 함수와 변수, this값 할당 및 저장
            Object Environment Record:
        },
        The outer environment reference: 외부 LE
    },
    Variable Environment: {
    	// LE와 같음
    }
}

참조사이트

atercatus | Github

ui.dev

Amandeep Singh | Medium

TOAST Meetup | NHN Cloud

0개의 댓글