우리가 작성한 JavaScript코드는 웹브라우저의 엔진을 통해 해석하고 실행합니다.
하지만 자바스크립트 엔진만으로 충분하지 않아서 브라우저가 제공하는 JS런타임 환경이 Web API, Queue, Event Loop 등을 동원합니다.
Runtime
자바스크립트 엔진(Single Thread)
자바스크립트 엔진 중 가장 유명한 것이 크롬의 V8엔진입니다. V8엔진은 크롬과 node.js 등 에서 사용됩니다. V8엔진을 간단히 표시한 모습은 이렇습니다.
V8엔진은 크게 두 부분으로 구성됩니다.
- 메모리 힙(Memory Heap): 메모리 힙은 변수 및 객체들을 메모리 할당하는 공간입니다.
- 콜 스택(Call Stack): 코드가 실행되면서 스택 프레임이 쌓이는 곳입니다. JS Engine은 단 하나의 CallStack을 갖고 있기 때문에 싱글 스레드의 동기적 방식으로 실행됩니다.
콜 스택
콜 스택은 기본적으로 우리가 프로그램의 어디에 있는지를 기록하는 자료 구조입니다.
함수는 호출 시 CallStack에 추가(push)됩니다. 최상위의 stack 함수가 실행되는 동안 하위의 함수들은 작업을 멈춘 채 대기합니다. 최상위의 함수가 작업을 완료한 후엔 CallStack에서 제거(pop)되며 return 값과 함께 이전 실행 함수로 되돌아갑니다.
콜스택 동작 예시
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
let squared = square(n);
console.log(squared);
}
printSquare(4);
엔진이 이 코드의 수행을 시작할 때 콜 스택은 비어있는 상태입니다.
코드를 실행하면 코드 자체를 말하는 메인 함수를 스택에 집어넣습니다.
콜 스택의 각각은 스택 프레임(Stack Frame)이라고 부릅니다.
하나씩 push 됩니다.
작업을 완료하면 하나씩 pop 되면서 다시 printSquare까지 돌아옵니다. console.log가 실행되면 리턴은 보이지 않지만, 암묵적으로 리턴됩니다.
스택 날림(Blowing the stack)
콜 스택의 최대 크기에 다다랐을 때 나타나는 현상입니다.
foo 함수를 호출하는 foo함수가 있다면 어떻게 될까요?
function foo(){
return foo();
}
foo()
스택 프레임이 계속 쌓이면서 어느 순간이 되면 콜 스택의 수가 실재 콜 스택의 크기를 넘게 되고 브라우저가 에러를 던집니다.
Blocking
블로킹은 느리게 동작해서 콜 스택을 막는 코드가 존재할 때 발생하는 현상입니다.
네트워크 요청이나 이미지 프로세싱 같이 느린 동작이 스택에 남아있으면 다음 스택 프레임이 작동하지 않으면서 끝날 때까지 마냥 기다려야 합니다.
유려한 ui를 만들려고 한다면, 콜 스택을 멈추게 해서는 안됩니다.
이걸 해결하기 위해서 비동기 콜백을 사용합니다.
Asynchronous
자바스크립트 엔진은 한 번에 하나의 일 밖에는 할 수 없기 때문에 브라우저가 Web API와 같은 것들을 제공해서 멀티 스레드를 지원합니다.
Web API(Multi Thread)
웹 API는 브라우저에 내장되어 작업을 실행하기 위한 다양한 추가 기능을 제공합니다. 웹 API 기능의 다수는 blocking이 일어나는 것을 방지하기 위해 비동기로 작동합니다.
Queue
Web API에서 실행 완료 후 전달된 Task(callback)가 CallStack에 추가되기 전 대기하는 곳입니다. Event Loop가 Task를 실행할 수 있는 특정 시점을 확인했을 때, 이벤트 루프는 Queue에 가장 오래된 Task를 제거하고 CallStack에 추가합니다.
Event Loop(Single Thread)
이벤트 루프는 Queue의 Task를 CallStack에 전달하는 중계자 역할을 합니다.
이벤트 루프는
- callStack에 실행 중인 함수가 없다.
- Queue에 Task가 존재한다.
위의 두 조건이 모두 true일 때 만 작동합니다. 즉 이벤트 루프는 콜 스택을 주시하고 있다가 비어있을 때만 큐의 코드를 콜 스택에 밀어 넣습니다.
console.log("hi");
setTimeout(function cb() {
console.log("there");
}, 0);
console.log("JS");
/*
hi
JS
there
*/
setTimeout에 0초를 주어도 가장 나중에 콘솔에 찍히는 이유도 이벤트 루프는 스택이 비어있을 때만 태스크 큐의 코드를 밀어 넣기 때문입니다.
Rendering
이런 것들이 렌더링과 어떤 관계가 있는지 설명합니다.
브라우저는 자바스크립트로 하는 무언가로 인해 제약을 받습니다.
브라우저는 기본적으로 화면을 매 16.6밀리 세컨드, 즉 1초에 60 프레임을 repaint 하는 게 이상적입니다. 그게 제일 빠른 거죠. 하지만 브라우저 자바스크립트가 하는 일들로 인해 여러 가지 이유로 제약을 받습니다. 그래서 스택에 코드가 있으면 렌더링을 못합니다. 렌더도 하나의 콜백처럼 행동하기 때문입니다.
다른 점이라면 콜백 큐에 비해 더 높은 우선순위를 갖습니다. 16.6 millisecond 마다 큐에 렌더가 들어가고, 스택이 깨끗해진 후에야 렌더링을 합니다.
하지만 코드를 실행하면, 우리가 느린 동기식 루프를 진행하는 동안 렌더는 막히게 됩니다. 렌더가 막히면, 화면의 텍스트를 선택하거나 클릭해서 반응을 보거나 하는 게 불가능해집니다.
사람들이 event loop를 막지 말라고 할 때, 바로 이런 현상을 뜻하는 것입니다. 스택에 필요 없이 느린 코드를 쌓아서 브라우저가 할 일을 못하게 만들지 말아라, 유동적인 UI를 만들어라 같은 게 이런 의미이죠.
스크롤 이벤트 같은걸 넣으면 스크롤을 할 때마다 엄청나게 많은 콜백들이 큐에 쌓입니다. 그리고 이걸 매번 처리하면서 스택을 채우지는 않지만 큐를 이벤트로 범람시키죠. 그래서 이것을 통해 많을 콜백들을 작동시킬 때 어떤 일이 일어나고 어떤 식으로 대처할지 알아야 합니다. 예를 들어 큐에 이벤트가 쌓이는 것은 어쩔 수 없지만 유저가 스크롤을 멈출 때까지 작업량을 줄인다든지 하는 결정을 내릴 수 있습니다.
레퍼런스
https://www.youtube.com/watch?v=8aGhZQkoFbQ
https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
https://dkje.github.io/2020/09/20/AsyncAndEventLoop/#블로킹blocking-논블로킹nonblocking