ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript] 4. 내부구조 및 실행과정
    JS 2021. 4. 25. 02:55
    728x90

    자바스크립트 언어를 공부하는데 있어 가장 중요하다고 생각하는 부분입니다.

    돌아서면 헤깔리는 부분이라 최대한 이해하기 쉽게 정리해보겠습니다.

     

    ✔︎ 자바스크립트 프로그램의 실행 과정

    실행 가능한 코드 찾기 => 해당 코드 평가 => 실행 문맥 생성

     

    ✔︎ 실행 가능한 코드 찾기

    자바스크립트 엔진은 3가지 유형( 전역 코드, 함수 코드, eval 코드  )의 실행 가능한 코드를 찾습니다.

    3가지 유형을 나누는 이유는 실행 문맥의 초기화 과정이 조금씩 차이가 나서 그렇습니다.

    특히 eval 코드는 앞으로 설명하게될 어휘적 환경 (Lexical Environment)이 아닌 다른 별도의 환경에서 실행됩니다.

     

    ✔︎ 실행 문맥 ( Execution Context )

    자바스크립트의 코드가 실행되고 관리되는 공간을 의미하며 코드가 실행되는데 필요한 모든 정보가 저장되어 있습니다.

    실행 문맥은 아래와 같은 구성으로 이루어져 있습니다.

     

    [ 실행 문맥 ]      =>     [ 디스 바인딩 ]

                                      [ 렉시컬/변수 환경 컴포넌트 ]      =>      [외부 렉시컬 환경 참조]

                                                                                                  [환경 레코드]      =>      [선언적 환경 레코드]

                                                                                                                                      [객체 환경 레코드]

     

    🚀 디스 바인딩 ( This Binding )

    자바스크립트 문법 중에서 가장 직관적으로 이해가 안되는 부분이라고 생각합니다.

    해당 내용을 나중에 다루도록 하겠습니다.

    지금은 우리가 알고있는 this가 실행 문맥에 저장되어 있으며, 저장된 값은 함수를 호출한 객체의 참조가 저장되는 곳이라는 겁니다.

     

    🚀 렉시컬 / 변수 환경 컴포넌트 ( Lexical / Variable Environment Component )

    렉시컬과 변수 환경 컴포넌트는 대부분의 내부 값이 동일하여 렉시컬 변수 환경 컴포넌트로 동일하게 취급해서 다루도록 하겠습니다.

    렉시컬 환경 컴포넌트는 자바스크립트 엔진에서 찾은 실행 가능한 코드를 실행하기 위한 정보들을 모아둔 곳입니다.

    구체적으로 유효 범위(Scope) 안에 있는 식별자와 결과값이 Key Pair로 저장되며, 추가로 해당 함수를 둘러싸고 있는 외부 영역에 대한 참조를 관리하는 곳입니다.

     

         👻 환경 레코드 ( Environment Record )

         유효 범위(Scope) 안에 있는 식별자와 결과값이 Key Pair로 저장되는 곳입니다.

         환경 레코드는 크게 선언적 환경 레코드와 객체 환경 레코드로 구성됩니다.

             

               🧨 선언적 환경 레코드 ( Declarative Environment Record )

               사용자가 선언한 함수와 변수 그리고 catch 문의 식별자와 실행 결과를 Key Pair로 저장하는 곳입니다,

     

               🧨 개체 환경 레코드 ( Object Environment Record )

               실행 문맥 외부에 별도로 저장된 객체의 참조를 통해서 데이터를 읽거나 씁니다.

               즉, with 문의 렉시컬 환경이나 전역 객체처럼 별도의 개체에 저장된 데이터는 개당 개체의 Reference를 가져와서

               개체 환경 레코드의 bindObject 프로퍼티에 저장하여 사용합니다.

     

         👻 외부 렉시컬 환경 참조 ( Outer Lexical Environment Reference )

          해당 함수를 둘러싸고 있는 외부 영역에 대한 참조를 관리하는 곳입니다.

          자바스크립트는 함수를 중첩으로 정의할 수 있는 언어라는 점에서 위과 같은 특성을 가지게 됩니다.

     

     

    ✔︎ 예시

    대충 느낌이 오셧으면 좋겟지만 저도 해당 내용 자주보긴하지만 볼떄마다 새로운 녀석입니다 ,,,

    그래서 프로그램이 실행될떄 어떻게 진행되는지 살펴보겠습니다.

     

    🚀 처음에 전역 환경이 생성될때를 보시면 다음과 같이 값들이 할당됩니다.

     

    [ 실행 문맥 ]   =>   [ 디스 바인딩 : window]

                                 [ 렉시컬/변수 환경 컴포넌트 ]   =>   [외부 렉시컬 환경 참조 : null]

                                                                                       [환경 레코드]   =>   [선언적 환경 레코드]

                                                                                                                     [객체 환경 레코드]   =>   [Bind Onject : window]

     

     

    🚀 전역 환경과 객체가 생성된 다음에는 실행가능한 코드를 읽어 들입니다. 

          해당 내용은 새로운 유효범위에 접근할때도 동일하게 진행됩니다.

     

          우선 선언된 변수 및 함수 선언문을 찾습니다.   " var x = 1 " 다음과 같이 전역에 선언되어 있다면 선언적 환경 레코드에 저장됩니다.

          앞선 포스트에서 말씀드린바와 같이 var는 선언과 초기화가 동시에 이루어져서 undefined의 값으로 저장됩니다.

          스코프 최상위 레벨에서 선언된 변수와 함수는 프로그램을 평가하는 시점에 객체 환경 레코드에 저장되게 됩니다.

          그래서 해당 변수 및 함수를 선언 이전에 참조해서 사용이 가능하며, 이를 호이스팅이라고 합니다.

     

    [ 실행 문맥 ]   =>   [ 디스 바인딩 : window]

                                 [ 렉시컬/변수 환경 컴포넌트 ]   =>   [외부 렉시컬 환경 참조 : null]

                                                                                       [환경 레코드]   =>   [선언적 환경 레코드 : Object { x : undefined }]

                                                                                                                     [객체 환경 레코드]   =>   [Bind Onject : window]

     

    ✔︎ 실행 문맥 스택 ( Execution Context Stack : EC Stack )

    갑자기 우리가 알고있는 자료구조 스택이 나옵니다. 이게 뭔가 싶으시겠지만 간단합니다.

    앞서 알아본 실행 문맥이 스택 구조로 관리된다는 뜻이고 쉽게 EC Stack이나 Call Stack 이라고 많이들 부릅니다.

     

    프로그램을 실행할때 새로운 유효범위에 진입할때마다 실행 문맥이 생성됩니다.

    이렇게 생성된 실행 문맥은 스택에 push 됩니다. 그렇기에 전역 실행 문맥은 스택의 맨 아랫부분에 위치하게 됩니다.

    프로그램 실행 순서는 스택에서 pop하는 순서대로 진행됩니다.

     

    이러한 특징의 이유는 자바스크립트가 싱글 스레드 방식으로 실행되기 때문입니다.

    즉, call stack에 쌓인 실행 문맥들을 위에서 부터 하나씩만 처리해 나갑니다.

    그래서 비동기 처리의 경우 해당 작업이 완료 된 이후 별도의 이벤트 큐에 저장해두었다가 현재 실행중인 컨텍스트가 pop된 이후에 이벤트 큐의 대기행렬을 차례대로 push해서 처리하게 됩니다.

     

    함수가 종료되면 실행 컨텍스트가 스택에서 pop되면서 해당 정보들은 모두 메모리에서 지워집니다.

    하지만 함수 바깥에서 해당 컨텍스트로의 참조가 유지된다면 가비지 컬렉터가 해당 정보를 지우지 않게 됩니다.

    이를 우리는 클로져라고 합니다.

     

    ✔︎ 디스 바인딩

    위에서 아주 조금이나마 자바스크립트 엔진이 동작하는 방식에 대해 이해하셨을거라고 생각합니다.

    그렇다면 이제 지멋대로인 this를 이해하실 수 있으실 겁니다.

    this는 앞에서 언급한 바와 같이  함수가 호출되었을 때 함수를 호출한 객체의 참조가 저장되는 곳 입니다.

     

     

    🚀 최상위 레벨 코드의 this

    전역객체를 가르킵니다.

    위의 예제처럼 신행 문맥이 생성될 때 this 바인딩이 전역 환경을 가르키도록 초기화 되기 때문입니다.

     

    🚀 이벤트 처기기 안의 this

    이벤트가 발생한 개체( 이벤트 처리를 등록한 개체 )를 가르킵니다.

     

    🚀 생성자 함수 안의 this

    그 생성자로 생성한 개체를 가르킵니다.

     

    🚀 직접 호출한 함수 안의 this

    최상위 레벨 코드에서 호출 시, 당연하게도 전역객체를 가르킵니다.

    코드 앞에 객체가 없으므로 자동으로 디스 바인딩이 전역 개체를 가르킵니다.

     

    🚀 call / apply 메소드를 호출한 함수 안의 this

    알고 계시겠지만 해당 메소드를 사용하면 this가 가르키는 개체를 변경할 수 있습니다.

     

     

    ✔︎ 유효 범위 체인 ( Scope Chain )

    앞서 실행 컨텍스트를 생성할 때 외부 렉시컬 환경 참조해당 함수를 둘러싸고 있는 외부 영역에 대한 참조를 관리하는 곳이라 하였습니다.

    이제 해당 내용를 유효 범위 체인과 함께 설명해보도록 하겠습니다.

    var one = 1;
    
    function f1() {
      var two = 2;
        
      function f2() {
            var three = 3;
            console.log(one + two + three);
      }
    	
      f2();
    }
    
    f1(); // 출력 => 6

    위의 예제를 보시면, 전역 컨텍스트가 생성되면서 선언적 환경 레코드에 변수 one과 함수 f1이 저장될 것입니다.

    이후 f1 함수를 실행하면, f1 함수에 대한 컨텍스트가 생성되면서 변수 two과 함수 f2가 저장될 것입니다.

    그리고 마지막으로 f2 함수가 실행되면서, f2 함수에 대한 컨텍스트가 생성 c가 변수 three 생성 이후 console.log 가 실행됩니다.

     

    그렇다면 f2 함수의 컨텍스트에는 변수 c 만 들어있는 상태인데 어떻게 출력 값이 6이 나오게 되는걸까요?? 그 답은 유효 범위 체인입니다.

    만일 우리가 필요한 변수가 선언적 환경 레코드에 없는 경우, 즉 유효 범위에 없는 경우 외부 렉시컬 환경 참조를 따라서 상위 환경 레코드를 검색합니다. 그리고 유효 범위 체인 안에서 식별자를 찾지 못하는 경우 Reference Error가 발생합니다.

     

     

    ✔︎ 클로져 ( Closure )

    프로그래밍 언어적 관점의 클로져는 자신이 정의된 환경에서 함수 안에 있는 자유 변수의 식별자 결정을 실행하는 자료 구조의 모음입니다.

    음,, 무슨 이야기지 라고 생각하실 겁니다. 앞서의 유효 범위 체인 과정을 생각해보시면 쉽게 이해 가실 겁니다. 

     

    클로져의 의미를 보는데 자유 변수라는 개념이 나옵니다. 자유 변수란 함수의 인수와 지역 변수가 아닌 변수를 모두 자유 변수라 하고

    함수의 인수와 지역 변수를 속박 변수라고 합니다. f2 함수에서 속박 변수는 three, 자유 변수는 one, two입니다.

    f2 함수에선 자유 변수 one과 two의 식별자를 결정합니다.

    즉, 클로져란 자유 변수의 식별자를 결정할 수 있는 함수 개체와 렉시컬 환경 컴포넌트의 집합이라고 할 수 있겠습니다.

     

    의미는 어느정도 이해가실 겁니다.

    그런데 이걸 클로져라는 이름을 붙여가면서 자바스크립트 문법의 핵심적인 기능 중 하나라고 하는 이유가 뭘까요??

     

    🚀 참조가 유지되는 경우 가비지 컬렉션의 대상에서 제외

    자바스크립트는 가비지 컬렉션을 활용하여 불필요한 개체를 메모리에서 자동으로 해제합니다.

    클로져의 자유 변수를 식별하는 과정에서 상위 개체에 대한 참조를 가지게 됩니다. 이로 인해 가비지 컬렉션에 대상에서 제외됩니다.

     

    🚀 캡슐화

    클로져는 OOP의 핵심적인 특징 중 하나인 캡슐화를 지원합니다.

    아래 예제를 보시면 전역에 선언된 counter1 변수는 counterFunc 함수의 리턴 값인 func 함수를 참조합니다.

    counterFunc 함수의 count 변수는 함수 외부에서 접근이 불가능한 숨겨진 상태로 counter 함수가 캡슐화된 개체처럼 볼 수 있습니다.

    즉, 클로저의 내부 상태 (외부 함수의 지역 변수, 선언적 환경 레코드)는 외부에 은폐되어 있어 중첩 함수 안에서만 읽거나 쓸 수 있습니다.

    추가적으로 함수 선언시 각기 다른 렉시컬 환경이 생성되서 counter1과 counter2의 결과는 별개입니다.

    function counterFunc() {
        var count = 0;
        return func;
        
        function func() {
        	return ++count;
        }
    }
    
    var counter1 = counterFunc();
    var counter2 = counterFunc();
    
    console.log(counter1()); // 출력 : 1
    console.log(counter1()); // 출력 : 2
    console.log(counter1()); // 출력 : 3
    
    console.log(counter2()); // 출력 : 1
    console.log(counter2()); // 출력 : 2
    

     

      👻  함수를 개체처럼 생성해 사용

    function Person(age, gender) {
        var age = age;
        var gender = gender;
        
    	return {
        	getAge : function() { return age; },
            getGender : function() { return gender; },
            setAge : function(newAge) { age = newAge; }
        };
    }
    
    var person = Person(27, "male");
    
    console.log(person.getGender()); // 출력 : male
    console.log(person.getAge()); // 출력 : 27
    person.setAge(28);
    console.log(person.getAge()); // 출력 : 28
    

     

      👻  커링

    function makeSum(x) {
        return function(y) {
        	return x + y;
        }
    }
    
    var sum1 = makeSum(1);
    var sum2 = makeSum(2);
    var sum3 = makeSum(3);
    
    console.log(sum1(10)); // 출력 : 11
    console.log(sum2(20)); // 출력 : 22
    console.log(sum3(30)); // 출력 : 33

     

      👻  초기화 기능

    function makeSum(x) {
        var SquaredX = x * x;
        // 렉시컬 환경을 동일한 변수에 선언시 모두 공유하기 때문에 만일 초기화에 많은 시간을 소모한다면 효과적일겁니다.
    
        return function(y) {
        	return SquaredX + y;
        }
    }
    
    var sum = makeSum(10);
    
    console.log(sum(10)); // 출력 : 110
    console.log(sum(20)); // 출력 : 120
    console.log(sum(30)); // 출력 : 130

     

      💩 반복문 ( 잘못된 사용 )

    function count() {
        for (var i = 0; i < 3; ++i) {
        	console.log(i)
        }
    }
    
    function count2() {
        for (var i = 0; i < 3; ++i) {
            setTimeout(function() {
                console.log(i)
            }, 3000)
        }
    }
    
    count();
    // 결과 : 0
    // 결과 : 1
    // 결과 : 2
    
    count2();
    // 결과 : 3 3 3

     

      👻  반복문 ( 해결 방법 )

    function count3() {
        for (var i = 0; i < 3; ++i) {
            (function(j) {
                setTimeout(function() {
                    console.log(j);
                }, 3000);
            })(i);
        }
    }
    
    function count4() {
        for (let i = 0; i < 3; ++i) {
            setTimeout(function() {
                console.log(i);
            }, 3000);
        }
    }
    
    
    count3();
    // 결과 : 0
    // 결과 : 1
    // 결과 : 2
    
    count4();
    // 결과 : 0
    // 결과 : 1
    // 결과 : 2

    'JS' 카테고리의 다른 글

    [Javascript] 6. 이벤트 처리  (0) 2021.04.25
    [Javascript] 5. 클라이언트 측 자바스크립트  (0) 2021.04.25
    [Javascript] 3. 함수  (0) 2021.04.22
    [Javascript] 2. 변수 및 개체  (0) 2021.04.22
    [Javascript] 1. 언어적 특징  (0) 2021.04.22
Designed by Tistory.