JavaScript는 CallStack, EventQueue 이용하여 Single Thread 기반으로 비동기적으로 동작한다. Single Thread 기반에서는 단 하나의 CallStack만 존재하기 때문에 네트워크 요청 등 비용이 많이 드는 연산을 수행하게 되면 매우 비효율적으로 동작된다. 이러한 문제점을 어떻게 해결하는지 하나씩 확인해보자.

architecture

Javascript Engine

위 그림같이, JavaScript Engine은 주요하게 두 가지 구성 요소를 가지고 있다.

  • Heap Memory : 객체, 전역 변수 등을 위한 메모리 할당, 구조화되지 않은 넓은 메모리 영역

  • Stack Memory : 함수의 CallStack, 지역 변수를 위한 메모리 할당

Runtime

JavaScript는 단일 스레드 기반이기 때문에 호출 스택이 하나만 존재한다. 동기적인 관점으로 바라보면 동시에 여러개의 태스크를 실행할 수 없다. 이러한 단점을 해결하기 위해, 프로그램은 자바스크립트 엔진이 아닌 Web API(DOM, AJAX, TIMEOUT, 비동기 함수 지원) 이용하여 비동기적으로 동작할 수 있게 된다. 예로 들어 보면 아래와 같다.

  • 기본적으로 JavaScript는 CallStack에 프레임을 push 하고 순차적으로 pop 하며 함수를 실행한다.
  • 실행 중간에 Web API setTimeout()가 호출되면, 해당 함수는 Web API 영역에서 비동기적으로 실행된다.
  • 실행 Context는 Block 되지 않고 바로 다음 명령을 실행한다.
  • Web API가 완료되면, CallBack 함수를 CallBack Queue에 enqueue한다.
  • CallStack 비어있을 때, Call Queue에서 CallBack 함수를 pop 하여 CallStack에서 실행한다.

위와 같은 일련의 과정을 통해, JavaSrcipt는 처리 비용이 큰 작업을 Web API 이용하여 비동기적으로 해결한다고 볼 수 있다. 따라서 단일 스레드 단점을 극복할 수 있는 것이다.

Call Stack

스택은 FILO(선입 후출) 자료구조로 함수 호출 단위를 스택 프레임에 쌓아가고, 각각은 호출 함수에 대한 복귀 주소를 기억하고 있다. 여러 언어로 개발을 해보며 Stack Trace 볼 수 있었을 것이다. 예외가 발생한 스택 프레임부터 복귀 주소로 회귀하며 Stack Tracing 하는 것은 스택 프레임 구조이기에 가능한 것이다.

Event Queue

setTimeout() 함수에 지연시간을 0 으로 주는 예를 보자.

function a() {
    console.log('a');
}

function b() {
    a();

    setTimeout(function() {
        console.log('b');
    }, 0);

    c();
}

function c() {
    console.log('c');
}

b();

위 예제는 순차적으로 a, b, c 가 출력될 것이라고 생각하지만, 실은 a, c, b 순서로 출력된다. setTimeout() 지연 시간을 0을 넣었는데 의아할 수 있지만, setTimeout() 함수는 Web API 에서 지원하는 함수로 비동기로 실행되고 콜백함수는 Event Queue에 enqueue 되며 c() 함수가 CallStack으로 들어가게 된다. c() 함수는 CallStack에서 바로 출력하지만, b() 함수는 Event Queue에서 CallStack이 모두 비어질 때까지 대기하고 CallStack에서 처리된다.

Event loop

while(queue.wait()) {
queue.process();
}

  • queue.wait() 함수는 현재 아무 메시지도 없다면 새로운 메시지 도착을 동기적으로 기다린다.

참고

온라인 사이트