Javascript와 웹 브라우저가 작동하는 전체적인 그림을 표현하면 위와 같습니다.
Javascript 엔진에 heap과 콜 스택이 있으며 콜스택에 있는 함수들이 하나씩 pop
되면서 해당 함수가 작동하게 됩니다. 이렇게 Javascript는 하나씩 pop
되면서 한번에 하나의 동작밖에 실행하지 못하고 하나의 콜 스택만을 가지는 싱글스레드 언어입니다.
그래서 Javascript는 비동기 동작을 처리하기 위해 브라우저에서는 Web API, Node.js에서는 libuv라는 라이브러리를 이용하여 논-블로킹 I/O를 지원합니다.
🧱 여기서 논-블로킹 I(Input)/O(Output)이란, I/O 처리가 완료처리 됐을 때 결과를 회신하는 I/O 모델입니다.
여기서 Web API에 있는 DOM, ajax, setTimeout과 같은 것들은 Javascript 엔진인 V8에 존재하는게 아니며, 웹 브라우저에서 제공하는 Web API에 존재합니다.
Web API에 내장되어 있는 setTimeout
함수나, DOM 객체에 존재하는 onClick
이벤트는 이벤트 루프라는 것에 의해 관리되고 실행되는데요, 간단하게 아래의 예제 코드는 다음과 같은 순서로 작동하게 됩니다.
setTimeout(() => {
console.log(2);
}, 0);
console.log(1);
console.log(2)
를 태스크 큐(위 그림에선 콜백 큐)에 넣습니다.console.log(1)
가 콜 스택에 올려집니다.console.log(1)
가 콜 스택에서 비워지면서 실행됩니다.console.log(2)
를 콜 스택에 올립니다.console.log(2)
가 실행됩니다.위에서 Javascript의 작동원리를 간단하게 보았는데요, Web API의 함수들이 태스크 큐에 추가되고, 콜 스택이 비워지면 이벤트 루프가 태스크 큐에 있는 함수를 콜 스택에 올린다는 사실을 알 수 있었습니다.
(참고로 FIFO, 먼저 추가된 태스트가 가장 먼저 나가는 구조 즉, 큐입니다)
그런데 이 이벤트 루프는 마이크로 태스트 큐도 지켜보는데요, 이 마이크로 태스크 큐에는 Promise의 then 메소드를 추가시키는데요, 여기서 추가된 마이크로 태스크는 일반 태스크보다 우선순위를 가지고 있습니다.
따라서 아래 코드의 출력결과는 다음과 같습니다.
setTimeout(() => {
console.log('A');
}, 0);
Promise.resolve().then(() => {
console.log('B');
}).then(() => {
console.lob('C');
});
B
C
A
Promise의 then
메서드가 마이크로 태스크로서 우선순위를 가지게 됨으로, 콘솔에는 B
와 C
가 먼저 찍히게 됩니다. 그 이후 setTimeout
의 A
가 찍히게 되죠.
Javascript는 싱글 스레드 언어이기 때문에 하나의 콜 스택을 가지고 여러가지 동작을 처리하게 되면 화면 렌더링이 느려저 최악의 사용자 경험을 제공하게 됩니다.
이를 해결하기 위해, 웹 브라우저에서는 Web API, Node.js에서는 libuv라는 라이브러리를 이용하여 비동기 동작 즉, 논-블로킹 I/O를 처리하며, 이벤트 루프는 이를 관장하여 콜 스택이 비워졌을 때 태스크 큐, 혹은 마이크로 태스크 큐에 있는 작업들을 콜 스택으로 올려줌으로써 멀티 쓰레딩처럼 작업합니다.