자바스크립트는 싱글쓰레드기반의 언어인데 어떻게 비동기로 동작을 할까?
먼저 아래 영상을 보면 이해가 쉬울 것이다.
한국어 자막 ON.
사실 자바스크립트가 비동기로 동작하는 이유는 브라우저에 있다.
브라우저는 Web API
, Callback Queue
, Event Loop
등으로 구성되어있고 자바스크립트 코드가 실행될 때 브라우저와의 동작은 아래 그림으로 표현할 수 있다.
브라우저의 자바스크립트 엔진은 Memory Heap
과 Call Stack
으로 구성되어있다.
자바스크립트는 싱글쓰레드기반의 언어기 때문에 한 번에 하나의 일만 처리할 수 있다. 즉, 선입후출(LIFO, Last In First Out)방식이다.
WebAPIs는 브라우저에 제공하는 API로 DOM(document)
, AJAX(XMLHttpRequest)
, Timeout(setTimeout)
등이 있다.
Callback Queue
동기적으로 실행된 콜백함수가 보관 되는 영역이다.
Callback Queue는 선입선출 FIFO(Frist In Frist OUT)방식이다.
Event Loop
는 Call Stack과 Callback Queue의 상태를 체크하여,
Call Stack이 빈 상태가 되면, Callback Queue의 첫번째 콜백을 Call Stack으로 밀어넣는다.
싱글쓰레드는 하나의 함수가 실행되면 이 함수의 실행이 끝날 때까지 다른 작업이 중간에 끼어들지 못한다.
함수가 실행되면 해당 함수는 Call Stack의 가장 상단에 위치하고, 함수의 실행이 끝날 때 해당 함수는 Call Stack에서 제거된다.
Single Thread의 단점은 브라우저에서 호출 스택에 실행할 함수가 쌓여있는 동안은 다른 일을 할 수 없다. 이 상태를 blocked
라고 한다. 이 상태에서 브라우저는 렌더링을 할 수도 없고, 다른 코드를 실행할 수도 없다. (브라우저의 alert 창이 떴을 때 blocked된 상태이다.)
//재귀함수로 무한루프에 빠지게되면 최대로 쌓을 수 있는 Stack의 갯수를 넘게된다.
var count = 0;
function stack() {
console.log(++count);
stack();
}
stack();
Stack Overflow
Stack의 최대 크기는 브라우저에 따라 다르고 버전에 따라 다르다고한다.[1]
Task queue는 callback queue와 같은 개념이다?
Callback queue
는Task queue
의 한 종류일 수 있음.
Task queue
는 html standard에서 사용되는 정식 명칭임.
~ Task queue에 대한 오해 정리 ~
사실 모든 비동기 동작이 Task Queue에 쌓이는 것은 아니고, 실제로는 여러 Queue가 존재한다.
ES6에 들어오면서 새로운 컨셉인 Microtask Queue
가 도입됐다고한다. Microtask Queue
는 Task Queue
와 동일한 계층에 존재하고 프로미스의 비동기 호출 시 Microtask Queue
에 쌓이게 된다.
실행 결과를 생각해 보자.
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");
만약 모든 비동기 호출이 단일 task queue
에 의해 관리되고, Event Loop는 호출 스택이 비었을 때 task queue
에서 순서대로 꺼낸다면 아래와 같은 결과일 것이다.
script start
script end
setTimeout
promise1
promise2
그러나 실제로는 아래와 같이 출력된다.
script start
script end
promise1
promise2
setTimeout
브라우저의 이벤트 루프가 task
와 microtask
를 어떻게 다루는지 알아보자.
//1. script 실행 (log)
console.log("script start");
//2. script 실행 (setTimeout callback task queue에 등록)
setTimeout(function () {
//9. Task 실행
console.log("setTimeout");
}, 0);
//3. script 실행 (Promise then callback Microtask queue에 등록)
Promise.resolve()
.then(function () {
// 6. MicroTask 실행
console.log("promise1");
}) // 7. script 실행 (Promise then callback Microtask queue에 등록)
.then(function () {
// 8. MicroTask 실행
console.log("promise2");
});
//4. script 실행 (log)
console.log("script end");
//5. Stack의 모든 Task 실행완료
Microtask
외에도 Queue는 또 있다. 바로 requestAnimationFrame에 의해 등록되는 Animation Frames
이다.
예제를 일부만 수정해서 Animation Frame을 추가해보자.
//1. script 실행 (log)
console.log("script start");
//2. script 실행 (setTimeout callback task queue에 등록)
setTimeout(function () {
//11. Task 실행
console.log("setTimeout");
}, 0);
//3. script 실행 (Promise then callback Microtask queue에 등록)
Promise.resolve()
.then(function () {
// 7. MicroTask 실행
console.log("promise1");
}) // 8. script 실행 (Promise then callback Microtask queue에 등록)
.then(function () {
// 9. MicroTask 실행
console.log("promise2");
});
//4. script 실행 (AnimationFrame Animation frames에 등록)
requestAnimationFrame(function () {
//10. Animation Frame 실행
console.log("animation");
});
//5. script 실행
console.log("script end");
//6. Stack의 모든 Task 실행완료
결과는 다음과 같다.
script start
script end
promise1
promise2
animation
setTimeout
이러한 동작들은 브라우저마다 호출 순서가 다를 수 있다. promise가 ECMA Spec이므로 브라우저마다 처리하는 방식이 다르기 때문이다. 특정 브라우저에서는 promise를 microtask로 처리하는 것이 아니라 task로 처리하는 경우도 있다.(이 글은 크롬 기준이다.)
자세한 비교를 보려면 아래 링크를 참고하자.
Tasks, microtasks, queues and schedules
어쨌든 이벤트 루프는 무엇입니까? | Philip Roberts | JSConf EU
Tasks, microtasks, queues and schedules
https://stackoverflow.com/questions/7826992/browser-javascript-stack-size-limit
https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html
Task queue에 대한 오해 정리
와 너무 쉽게 잘 설명한 글이네요! 잘 읽고 갑니다! :)