자바스크립트는 동기식 언어이다. 동기는 한 번에 하나의 작업을 실행하는 것을 뜻하고, 이는 싱글 스레드에서 코드가 동작하기 때문이다.
동기식 작업은 순서가 정해져 있기 때문에 설계가 단순해지고 안전하다는 장점이 있다. 다만, 자원을 효율적으로 사용하지 못한다는 단점이 있다.
반대로, 비동기는 요청의 응답에 관계 없이 다음 작업을 기다리지 않고 실행하는 것을 말한다. 멀티 스레드 작업 환경에서는 요청을 처리할 수 있는 작업 단위가 여러 개 존재하기 때문에 하나의 태스크의 실행 결과를 기다리지 않고 다음 태스크를 실행할 수 있다.
비동기식 작업 방식은 요청에 대한 응답이 늦어지더라도 응답 결과를 기다리는 동안 다음 작업을 실행할 수 있다. 하지만, 설계가 복잡해지고, 요청 결과가 다음 요청 결과에 영향을 미치는 경우 코드가 의도와는 다르게 동작할 수 있다.
자바스크립트 엔진은 자바스크립트의 코드를 분석하고 실행하는 인터프리터를 말한다. 자바스크립트 엔진은 주로 웹 브라우저에서 사용된다. 웹 브라우저에 내장되어 있기 때문에 웹 브라우저에서 곧바로 코드를 해석하고 실행할 수 있다.
객체는 변수이기 때문에 원시 데이터와 달리 데이터의 크기가 정해져 있지 않다. 할당 메모리의 공간 크기는 런타임 시에 결정되므로, 힙은 결국 객체를 저장하는 넓은 영역의 공간이자 구조화되지 않은 영역이다.
자바스크립트 엔진은 하나의 콜스택만을 사용하기 때문에 최상위 실행 컨텍스트가 실행되기 전까지는 다른 어떤 태스크도 실행되지 않는다.
앞서 살펴본 것처럼 자바스크립트는 동기식 언어다. 그렇다면 어떻게 비동기를 처리하는가? 이를 알기 위해서는 브라우저에 대해서도 알고 있어야 한다. 브라우저는 자바스크립트 엔진 외에도 태스크 큐, 이벤트 루프, Web API를 갖고 있다. 태스크 큐는 비동기 함수의 콜백 함수를 임시 보관하고, 이벤트 루프는 자바스크립트 엔진의 콜스택이 비었는지, 태스크 큐에 대기하고 있는 함수가 있는지 확인한다. 콜스택이 비었고, 태스크 큐에 함수가 있다면 이벤트 루프는 큐의 함수 하나를 콜스택에 보내 실행시킨다.
이 때, 이벤트 루프는 항상 함수 하나만을 가져 온다.
브라우저 엔진은 자바스크립트 엔진과 다르게 멀티 스레드로 동작한다. 멀티 스레드로 동작하는 브라우저가 Web API를 제공하여 비동기 실행을 구현할 수 있는 것이다.
브라우저 Web API
브라우저 환경에서 제공하는API
를 말한다.
브라우저 Web API는 작성된 함수의 콜백 함수를 받고 특정 동작을 수행한 뒤 이 콜백 함수를 큐로 넘겨주는 역할을 한다.
결국 자바스크립트 엔진은 동기식 동작을 하지만, 브라우저 엔진이 자바스크립트 엔진에서 전달 받은 일을 독립적으로 수행하기 때문에 자바스크립트의 코드가 비동기 방식으로 처리되는 것처럼 보인다.
마이크로태스크 큐
콜스택이 비어 있을 때 가장 먼저 이벤트 루프가 확인하는 큐로, Promise
의 then
, catch
, finally
가 있다.
태스크 큐
마이크로태스크 큐에 함수가 없을 때 이벤트 루프가 확인하는 큐로, setTimeout
, setInterval
, addEventListner
등이 있다.
큐를 나누는 이유
Promise
로 데이터를 전달 받아 웹 페이지에 반영하는 코드가 있을 때, 큐의 우선순위가 존재하지 않는다면setTimeout()
이 먼저 실행되어 원하는 데이터를 반영하기 전 페이지를 띄워야 하는 불상사가 발생할 수 있다.
브라우저는 HTML
과 CSS
를 Parsing하여 브라우저 화면을 표시하는 기능을 담당하는 렌더링 엔진을 제공한다.
브라우저의 렌더링 과정
HTML
을 Parsing하여 DOM
생성 CSS
를 Parsing하여 CSSOM
생성Render Tree
생성Render Tree
배치(Layout)Render Tree
그리기(Paint)이 때, 렌더링 작업은 이벤트 루프에 의해 태스크 큐에 저장된다. 이말은 즉슨, 렌더링이 다른 콜백 함수보다 높은 우선순위를 갖고 있지만, setTimeout
의 콜백 함수가 끼어 들거나 Promise
콜백 함수보다 낮은 우선순위를 갖는다는 문제가 있다. 그러면 다른 콜백 함수가 처리되는 동안 렌더링 엔진이 작동하지 못하고, 화면은 버벅이는 것처럼 보일 수 있다. 그래서 유동적인 UI를 위해서라면 이벤트 루프를 막지 말아야 한다.
// HTML
<body>
<button>Click me!</button>
<div id="loading"></div>
<div id="result"></div>
</body>
// Javascript
const testButton = document.querySelector("button");
const loadingBox = document.querySelector("#loading");
const resultBox = document.querySelector("#result");
let result;
function isLoading() {
loadingBox.innerHTML = "로딩 중";
}
function someProcessTakeLongTime() {
for (let i = 0; i < 100000000; i++) {
result += i;
}
}
function hideIsLoading() {
loadingBox.innerHTML = "";
}
function showResult() {
resultBox.innerHTML = "결과입니다.";
}
testButton.addEventListener("click", () => {
isLoading();
someProcessTakeLongTime();
hideIsLoading();
showResult();
});
위 코드에서 isLoading()
이 실행되어 "로딩 중"
이라는 문구를 띄운 뒤, someProcessTakeLongTime()
함수를 실행할 것이라 생각하는 분들은 반성해야 한다(그게 바로 나다..).
함께 살펴보도록 하자. 먼저 isLoading
함수가 콜스택으로 이동하고 실행된다. 그러면isLoading
함수에서 문구를 변경하기 위해 렌더링 요청을 보내는데, 렌더링 요청은 비동기적으로 동작하기 때문에 태스크 큐로 이동한다. 이제 콜스택이 비워지고 someProcessTakeLongTime()
함수가 콜스택으로 이동하고 실행된다. 나머지 함수가 실행되기까지 렌더링 요청에 대한 처리는 미뤄지고 결국 "로딩 중"
이란 문구는 확인할 수 없다.
이를 해결하기 위해 다음과 같이 코드를 작성할 수 있다.
testButton.addEventListener("click", () => {
isLoading();
setTimeout(() => {
someProcessTakeLongTime();
hideIsLoading();
showResult();
}, 0);
});
isLoading
함수가 콜스택으로 이동하여 실행된다. 그러면 렌더링 요청이 태스크 큐로 이동하고, 콜스택이 비워진다. 다음으로 setTimeout()
함수가 콜스택으로 이동하고 실행되는데, setTimeout
은 비동기적으로 동작하기 때문에 콜백 함수는 태스크 큐로 이동한다. 이제 콜스택이 비워지고 태스크 큐에서 대기하던 함수들이 실행되므로 "로딩 중"
이라는 문구를 확인할 수 있다.
--
🙇🏻♂️ https://paullabworkspace.notion.site/abfb023f97954ab8ae9d3ec8685a8415