3D Web Developer/Front_End [JS, React,Three,R3F]

[ 3D Web : Front_JavaScript ] JavaScript - 자스의 동작원리.

안다미로 : 담은 것이 그릇에 넘치도록 많게. 2024. 10. 19. 22:00

 

 

 [ 3D Web : Front_JavaScript ] JavaScript - 자스의 동작원리.

 

 


 

∇ 자바스크립트의 동작원리


목  차

1. 자바스크립트의 동작 구조.
2. 자바스크립트는 무슨 스레드 구조인가 (싱글?멀티?)
3. 자바스크립트가 비동기 작업을 수행하는 방법은?
4. Call Stack이 비어있어야 하는 이유는

 

 


1. 자바스크립트의 동작 구조.


        - 자바스크립트를 동작시키기 위해서는 " 자바스크립트 엔진 "이 필요합니다.

 

        - 자바스크립트 엔진은 V8, Rhino, SpiderMonkey 등 다양하게 있지만,

             이 중에서 가장 대표적인 예는 Google에서 제작한 V8엔진입니다.

V8 엔진 도식화

 

           -V8엔진은 두 가지 구성요소로 구성됩니다.

 

                       1. Memory Heap : 메모리 할당이 발생하는 곳.

                       2. Call Stack.. : 코드 실행에 따라 스택이 하나씩 쌓이는 곳.

 

 

           "자스"를 사용하면서 개발을 하다보면 setTimeOut()과 같은 수많은 API를 사용하게 됩니다.

 

           이런 API들은 자바스크립트 엔진에서 제공해주는 요소가 아닌,

                   Web API라고 해서 웹 브라우저 혹은 node.js 같은 자바스크립트 런타임에서 지원해주는 API입니다.

               ( 그래서, 브라우저별로 지원 여부가 다름 )

 

Web API

 

 

          자바스크립트는 위에서 언급한 setTimeOut()과 같은 비동기 처리 코드 작성이 가능함에도 불구하고,

          자바스크립트 자체에는 비동기 코드를 처리하기 위한 개념/설정을 가지고 있지 않습니다.

 

           이런 자바스크립트가 비동기 코드를 처리할 수 있는 이유는 바로

                   Event Loop & Callback Queue의 환상적인 조합때문입니다.

 

 

           위에서 말한 WebAPI와 런타임에서 Event Loop와 Callback Queue를 지원해줌으로써 동작 할 수 있는 것입니다.

 

EventLoop & Callback Queue

 

 


 

2. 자바스크립트는 무슨 스레드 구조인가? ( 싱글? 멀티? )


 

      자바스크립트가 멀티 스레드인지 단일 스레드인지 알아보도록 하겠습니다.

 

       V8엔진의 구조를 간단하게 봤을 때,

         Call Stack(호출 스택)과 하나의 Memory heap(메모리 힙)으로 이루어진 것을 확인 가능합니다.

 

      "Call Stack(호출 스택)"은 기본적으로 자바스크립트를 한 줄식 읽어가면서,

            우리 코드가 순서대로 작동하도록 보장해주는 데이터 구조입니다.

        스택 구조이기 때문에, 당연히  "후입 선출(LIFO, Last-In-First-Out)" 구조를 가집니다.

 

 

        "Call Stack"에 쌓이는 하나의 사각형을 "스택 프레임(Stack Frame)"이라고 하는데,

           

          함수가 실행이 된다면, 스택구조의 맨 위에 있던, '스택 프레임'을 가리키는 중이며,

          함수의 실행이 끝날 때, 해당 스텍 프레임을 "Call Stack" 상에서 제거하게 됩니다.

 

 

              ∑ ex) 예제 :  사각형의 넓이를 구하는 예제.

     

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

     

       

             초기에 엔진이 자바스크립트를 실행시킬 때는 아무 함수도 만나지 않은 상태라서 비어있지만,

 

             코드를 실행시키면, printSuare가 호출되고, printSquare를 Push해서 스택안에 쌓아놓고 읽어가다가

              이후에 만난 multiply를 Push해서 스택에 추가합니다.

             코드를 돌며 더 이상 쌓을 함수가 존재하지 않을 때 맨 위의 스택 프레임부터 하나식 처리하며 

               결과를 출력하게 됩니다.

스택에서 일어나는 모습

 

 

         이런 작동방식이 적용되는 스택이 만약 초과하게 된다면?

          무한 루프가 돌게되는 코드를 돌리면, Maximun call stack size라는 에러가 발생하게 됩니다.

 

       

 

       무한 루프 상황을 그림으로 표현한다면, 

               위의 그림처럼 표현 할 수 있습니다. 

               Call Stack은 정해진 스택 사이즈가 존재하고, 하나씩 쌓이기 때문에 정해진 용량을 초과하게 되면

                에러가 발생하게 됩니다.

               이것을 Stack OverFlow라고 합니다.

 

 

                이처럼 자바스크립트는 하나의 Call Stack만을 가지고 코드를 순차적으로 처리하기 때문에

                        한 번에 단 하나의 명령어만 실행될 수밖에 없습니다.

 

 

                이런 이유로 인해서 "자바스크립트"는 "단일 스레드"이며, "동기식 언어"라고 할 수 있습니다.

 

                 자바스크립트는 이러한 특성 때문에 

                 무한 루프는 발생할 수 있어도 동기화 문제인 교착상태(DeadLock)는 발생할 수 없습니다.

 

 


 

3. 자바스크립트가 비동기 작업을 수행하는 방법은?


 

 자바스크립트는 웹에서도 사용하고, node.js나 deno.js와 같은 런타임들과 함께 여러가지 작업을 수행 가능합니다.

 

 

     만일 자스의 단일 스레드 + 동기식 특성을 가진 Call Stack만을 사용해서 작업을 수행하려고 한다면

         현재의 많은 사이트들의 반응속도가 나올수가 없습니다.

 

 

    

     이 때 Event Loop & Callback Queue가 위력을 발휘합니다.

 

     

function foo() {
  console.log("1");
}

function foo2() {
  console.log("2");
}

foo();
setTimeout(function () {
  console.log("3");
}, 2000);
foo2();

 

위의 코드가  실행되면, 결과는 1,2,3으로 출력될 것입니다.

   하나의 Call Stack을 사용하기 때문에, 순서대로 스택에 들어가게 되면  1,3,2가 출력되는게 맞을텐데

       왜 다른 결과가 도출될까요?

 

    이유는 Event Loop & Callback Queue의 동작방식에 있습니다.

 

            1. 제일 먼저 foo()라는 함수가 Call Stack에 쌓이게 됩니다.

 

                     

              

 

            2. 그 다음, foo() 함수 안에 있는 console.log()가 Call Stack에 쌓이게 됩니다.

 

 

 

            3. 결과로 콘솔 창에 1을 출력합니다.

 

 

 

            3. foo() 함수는 종료되었으니 Call Stack에서 빠지고, setTimeOut()이 Call Stack으로 들어옵니다.

 

 

 

            4. 비동기 함수인 setTimeOut()은 WebAPI에서 처리하도록 보냅니다.

                  만약 node.js나 deno.js 같은 런타임의 경우에는 백그라운드에서 처리하도록 보냅니다.

                   그리고 그다음 함수인 foo2() 함수를 불러옵니다.

 

            5. foo2() 함수 안에 있는 console.log()가 Call Stack안에 쌓이게 됩니다.

 

 

            6. 콘솔 창에 2를 출력합니다.

 

 

            7. foo2()함수는 출력함으로써 종료되었으니, Call Stack에서 빠지게 됩니다.

               이제 새로 들어올 함수는 없는데,  아까 백그라운드(Web API)로 보냈던 비동기 함수가 처리중입니다.

 

              setTimeOut()의 처리 시간을 2000(2초)로 설정해 두었으니 2초간은 Web API상에서 처리하게 됩니다.

               2초가 지난 후에는 setTimeOut()의 콜백 함수를 Callback Queue로 보내게 됩니다.

 

              여기서 중요한 점은 Call Stack에 있던 foo2()가 함수가 종료되고 나서야 Web API를 처리하는 과정이 아니고

               setTimeOut()이 WebAPI로 넘어간 시점부터 이미 처리되고 있는 것입니다.(넘어갈 때부터 2초가 흐르고 있는 것)

 

          즉! Call stack에서 foo2() 함수를 처리하고 있었고, Web API에서는 setTimeOut()을 2초 동안 돌리고 있었다는 것!

 

 

 

 

 

            8.  이제 EventLoop가 등장합니다.   Event Loop는 Callback Queue에 있는 콜백 함수를 

                  Call Stack으로 보내서 처리하기 위해 Call Stack이 비어있는지를 검사합니다.

 

               Call Stack이 비어 있다면, Callback Queue에 있던 함수를 Call Stack으로 보내서 처리하게 됩니다.

 

 

            9.   Call Stack에 있던 console.log()를 콘솔에 출력하는 것으로 프로그램이 종료됩니다.

 

 


 

4.  Call Stack이 비어있어야 하는 이유는?


  Event Loop가 Call Stack이 비어있는지 여부를 확인하고서, Callback Queue의 함수를 처리하는 이유는?

 

 이벤트 루프가 반드시, Call Stack이 비어져있는 상태에서만 Call Stack으로 Push하는 이유를 간략히 말하면

      "자바스크립트라는 언어가 동기화 문제를 일으키는 것을 피하고 단일 스레드 언어의 상태를 보장해주기 위함"입니다.

 

  만약, 단일 스레드 환경에서,  Call Stack이 비어져있지 않은데,  Callback Queue의 내용을 push하게 되면

            멀티스레드의 문제를 단일 스레드 언어가 맞딱드리게 됩니다.

 

         따라서!

          단일 스레드 언어에서 stack에 있는 함수를 중단하지 말고, 실행이 모두 끝난 뒤

           이벤트 루프가 보낸 함수를 처리하는 방식의 동기화 문제를 해결하는 요소가 필요한 것입니다.