개요
자바스크립트가 어떻게 돌아가는지 알기 위해서, 자바스크립트 엔진에 대해 알아야 한다. Javascript 엔진이란, 자바스크립트를 실행시키는 인터프리터를 일컫는 말이다.
자바스크립트가 처음 나왔을 때는, 단순히 웹 페이지에서 Html Elements를 조작하는데에만 사용했다, 그러나 시간이 지남에 따라 Nodejs 등등 다양한 런타임 환경이 등장하면서 그 쓰임새는 다양해졌다.
현재 가장 유명한 JS 엔진으로 V8 엔진을 들 수 있겠다. V8 엔진은 구글 크롬에서 사용되며 Nodejs의 기반이 된 엔진이다.
구성 요소
자바스크립트의 엔진은 위 그림처럼 메모리 힙과 콜스택을 기반으로 한다.
메모리 힙은 동적으로 생성된 Object 들이 저장되는 곳이다. 콜스택과 다르게 메모리 힙에는 참조 타입들이 저장된다. 더욱 구체적으로는 메모리 힙에 어떤 주소에 객체의 정보가 저장되고, 해당 주소는 콜스택에 들어가게 된다. 또한 해당 주소를 나타내는 변수명도 콜스택 상의 렉시컬 환경(Lexical Environment)에 저장된다.
콜 스택은 방금 메모리 힙에서의 객체 주소 정보를 가지고 있기도 하며, 함수의 호출 정보를 저장하고 있다. 말 그대로 스택이기 때문에 FIFO 방식을 사용한다. 어떤 함수를 실행하면 해당 함수 프레임은 콜 스택의 가장 위로 들어가게 된다. 그리고 해당 함수가 종료되게 되면 콜 스택을 빠져나오게 된다.
자바스크립트는 싱글 스레드 기반이기 때문에 하나의 콜스택을 가지고 있다. 이는 즉 한번에 하나의 일만을 수행한다는 것이다.
비동기?
방금 자바스크립트는 싱글 스레드이며, 그렇기에 한번에 하나의 일만 수행할 수 있다고 했다. 그렇다면 자바스크립트를 설명하는 단어 중 하나인 '비동기'는 어쩌다 나오게 된 것일까?
사실 자바스크립트 혼자서는 비동기 로직을 수행할 수 없다. 다만 자바스크립트를 실행하는 런타임 환경 브라우저, 혹은 Nodejs 에서 이러한 것들을 할 수 있도록 도와주는 것이다.
상단의 그림에 런타임 환경을 더해서 확장해보면 다음과 같다.
setTimeout 이나 Ajax처럼 흔히 비동기 함수라고 불리는 것들은 사실 자바스크립트 내부가 아닌 외부의 API (브라우저라면 Web API, Nodejs라면 C++ API)로부터 제공받는 것이다.
간단하게 이런 코드가 실행된다고 해보자.
제일 먼저 콜스택에 main 함수가 들어간다.
그 뒤 빠르게 console.log 가 들어와서 hi! 를 콘솔에 찍고 나간다.
이후에 들어온 setTimeout은 WebAPI에 콜백을 등록하고 나간다. setTimeout같은 경우는 WebAPI 에 타이머가 등록됨과 동시에 해당 타이머가 시작된다.
이제 마지막으로 console.log('bye!') 가 콜스택에 들어오게 되고, 콘솔에 bye!를 찍음과 동시에 콜스택에서 나간다. 그리고 더이상 실행할 코드가 없으므로 main은 콜 스택에서 빠져나가게 된다.
이제 시간이 지나 setTimeout의 시간이 되었을때 콜백 함수는 이벤트 큐에 등록된다.이벤트 큐는 이름에서도 알 수 있듯이 큐의 성질을 가지고 있어서, 먼저 들어온 Message를 먼저 내보낸다. 이벤트 루프는 항상 이벤트 큐를 체크하며 내보낼 메시지가 있는지 확인한다.
지금 상황은 콜스택이 비어있으므로 이벤트 큐에서 이벤트 루프에 의해 바로 console.log 가 콜스택으로 이동한다. 여기서 중요한 점은, 이벤트 루프는 콜스택이 비어있을때만 메시지를 콜스택으로 보낸다는 사실이다. setTimeout의 delay로 0을 주면 코드의 실행 순서가 바뀌는 것도 바로 이 이유 때문이다. 바로 콜백함수가 실행되어야 할 것 같지만 실제로는 콜스택이 빌 때까지 기다리다가 실행되는 것이다.
이렇게 실행할 비동기 함수가 Web API로 넘어가서 실행된 뒤, 이벤트 큐에 등록되고, 지금 실행하는 코드의 흐름을 막지 않는 것을 Non-blocking 이라고 한다.
Nodejs에서는 어떨까?
Nodejs는 V8엔진 외에 libuv를 따로 포함하고 있다. 이 libuv 에서 필요한 비동기 로직들을 관리한다. libuv 내에 이벤트 큐가 존재하기 때문에 Nodejs의 모든 콜백 함수들은 libuv 내에서 관리하게 된다. libuv 내 이벤트 루프에는 여러 가지 페이즈들이 있어서 해당 페이즈들에 따라서 이벤트를 관리한다고 한다.
이렇게 나누어 놓은 이유는 브라우저와 다르게 OS에서 지원하는 비동기 함수들을 사용하기 위함이다. libuv는 이러한 작업들을 필요에 따라 libuv 자체에서 처리하거나, OS 커널에게 떠넘길 수 있다는 특징을 가지고 있다.
또한 파일 입출력과 같은 작업은 따로 워커 스레드에서 해당 작업이 완료될때까지 blocking하는 역할도 한다고 한다.
JSConf에 이벤트 루프에 대해 맛깔나게 설명해주신 영상이 있어서 도움을 많이 받았다.
'Study > JavaScript & Typescript' 카테고리의 다른 글
[JS] 클로저(Closure)와 렉시컬 스코핑 (0) | 2021.12.17 |
---|---|
[JS] 호이스팅(Hoisting)이란 무엇인가?? (0) | 2021.11.06 |
[JS] this 부터 call, apply, bind 까지 (0) | 2021.09.26 |
댓글