javascript 작동원리

Javascript

Javascript는 Java, C, Python 등의 언어와 다르게 싱글 스레드 기반의 언어로써 한번에 단 하나의 작업만을 처리할 수 있다. 즉, 비동기 처리 언어라는 것이다. 그러난 이는 Javascript 엔진에 국한된 얘기이다. 응? 무슨 소리인가? Javascript는 비동기 언어지만 이건 Javascript 엔진에 국한된 얘기라니? 실제로 Javascript를 구동시키기 위한 런타임 환경에는 Javascript Engine과 Web APIs, Envent Loop, Callback Queue가 존재하며 전체 런타임 환경에서 보면 동기 작동의 상황이 펼쳐지기도 한다.

Javascript Engine

가장 대표적인 Javascript Engine으로 구글에서 개발한 V8은 Chrome과 Node.js에서 사용한다. 아래의 사진은 V8 엔진의 구조도를 간단히 나타내고 있다.

javascript engine structure

위 사진과 같이 V8 엔진은 Memory Heap과 Call Stack으로 구성되어 있으며 정의는 아래와 같다.

  • Memory Heap : 메모리 할당이 일어나는 곳으로서 구조화되지 않은 넓은 메모리 영역을 지칭
  • Call Stack : 코드 실행에 따라 호출 스택이 쌓이는 곳 or 하나의 작업을 완료하기 위해 제공되는 환경
    • 호출 스택의 각 단계를 *스택 프레임(Stack Frame)*이라 한다.

서두에서 언급한 “Javascript는 … 싱글 스레드 기반의 언어로써 …” 에서의 싱글 스레드란 위에서 언급한 Call Stack을 의미한 것이었다. 따라서 한 번에 한 작업만 처리!

아래의 코드를 통해 Call Stack이 어떻게 작동하는지 보자.

1
2
3
4
5
6
7
8
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);

위 코드을 보면 printSquare(5) 가 호출되고 내부 실행 컨택스트인multiply(x,x)console.log가 차례대로 호출 및 실행 종료된 후printSquare(5) 가 종료되며 Call Stack이 빈다. 이는 Javascript의 Run to Completion이라고 하는데 하나의 함수가 실행되면 이 함수의 실행이 끝날때 까지 다른 어떤 작업도 중간에 끼어들지 못한다는 의미이다.

Call Stack

또한, 너무 많은 callback function이나 loop 문을 작동시키면 과도한 Task가 호출 스택에 쌓이게 되어 최대 허용치를 넘기게 되는데 이때 출력되는 에러가 바로 Uncaught RangeError: Macimum call stack size exceeded 이다. 보통 만 개 정도의 Task를 Call Stack에 쌓을 수 (같은 의미로 함수를 만 번 정도 호출할 수 있다는 의미) 있다는 의미로 위 에러가 발생했을시 function call 을 디버깅해봐야 한다.

그렇다면 서두에서 언급한 비동기적 상황 이란 무엇인가? 예를 들면 setTimeout이나 XMLHttpRequest 와 같은 비동기 호출 event 이다. 이러한 비동기 호출은 JS Engine이 아닌 Web API 영역에서 담당하는데 먼저 브라우저에서 Javascript가 작동하는 전체 환경을 보자.

brower-javascript-environment

JS Engine과 Web APIs 이외에 Event Loop와 Task Queue가 있는데 이 두 요소가 Web APIs에서 제공하는 함수는 Call Stack에서 비동기로 처리하지만 비동기 작업 후의 callback function은 Task Queue로 담기게 된다. 자세한 설명은 아래에서 이어하겠습니다.

브라우저 환경에서 Javascirpt를 구동시키기 위해서는 JS Engine 뿐만 아니라 Web APIs, Task Queue가 있으며 실제 자바스크립트가 구동되는 환경(브라우저, Node.js등)에서는 주로 여러 개의 스레드가 사용된다. 이러한 구동 환경이 단일 호출 스택을 사용하는 자바 스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 바로 ‘이벤트 루프’ 이다.

Event LoopTask Queue를 설명하기 위해 Ajax 통신을 생각해보자. 서버로부터 데이터를 요청하기 위해 Request를 요청하며 Response를 받기 위한 callback function이 있다고 가정해보자. 그렇다면 위 브라우저 환경에 따라 설명해보면 Ajax get 요청은 Call Stack에서 비동기로 처리한 후 Web APIs가 동작하는 별도의 환경에서 실행되어 완료된 후에는 Response를 받기 위한 callback function을 Task Queue에 push되어 Call Stack에서 Task가 모두 종료되어 빈 상태가 될까지 기다린 후 Event Loop에 의해 다시 Call Stack에 push되어 처리됩니다. 이러한 구동방식으로 자바스크립트는 이벤트 기반 동시성(Concurrency) 모델을 제공합니다. 이벤트 루프의 동작을 시각적으로 보시기 원하신다면 http://latentflip.com/loupe/ 를 보시기 바랍니다.

정리하자면,

  • Task Queue: 말 그대로 콜백 함수들이 대기하는 큐(FIFO) 형태의 배열
    • 모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가한다.
  • Event Loop: (함수)호출 스택이 비워질 때마다 큐에서 콜백 함수를 꺼내와서 실행하는 역할을 함
    • ‘현재 실행중인 태스크가 없을 때’(주로 호출 스택이 비워졌을 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다.
    • 언제나 그렇듯이, 함수를 호출하면 그 함수의 사용을 위한 새로운 스택 프레임이 생선된다.

위와 같은 구조적 동작 덕분에 JS Engine은 싱글 쓰레드 기반의 비동기 작업이 보장받지만 반대로 싱글 쓰레드 기반의 Javascript 작동 원리의 단점 이 발생하기도 합니다. 바로 Run-to-completionEvent Loop 때문이다. 만약 콜스택 내 함수의 수행시간이 길어져 Task Queue의 콜백 함수들이 수행되지도 못하거나 과도한 콜백 함수들이 연속적으로 수행될 경우 브라우저에서는 아무것도 할 수 없습니다. 이게 바로 블록킹 이 된다고 하는 것입니다. 이럴 경우 브라우저는 장시간 응답이 없을 수 있으며 에러를 일으켜 사용자에게 페이지를 닫을지 물어보기도 합니다. 이와 상황에서의 해결책이 바로 비동기 콜백(Asynchronous callbacks) 입니다.

참조