브라우저는 Web APIs, Event Table, Callback Queue, Event Loop 등으로 구성되며 자바스크립트 코드가 실행될 때 브라우저와의 동작은 아래 그림으로 표현할 수 있습니다.
구성요소
setTimeout(function exec() {
console.log('second')
}, 1000);
위 코드가 실행될 때 각 구성요소 들이 어떤 역할을 하는지 보겠다.
예제)
출력이 어떻게 나올지 잠깐 생각해보기 ! ! 5... 4... 3... 2.. 1..
console.log('first')
setTimeout(function cb() {
console.log('second')
}, 1000); // 0ms 뒤 실행
console.log('third')
위 코드가 실행될 때 각 구성요소들이 어떻게 동작하는지 순서대로 보겠다.
1.console.log(‘first’)가 Call Stack에 추가(push) 된다.
2. console.log(‘first’)가 실행되어 화면에 출력한 뒤, Call Stack에서 제거(pop) 된다.
3.setTimeout(function cb() {..}) 이 Call Stack에 추가된다.
4. setTimeout 함수가 실행되면서 Browser가 제공하는 timer Web API 를 호출한다. 그 후 Call Stack에서 제거된다.
5. console.log(‘third’)가 Call Stack에 추가된다.
6. console.log(‘third’)가 실행되어 화면에 출력되고 Call Stack에서 제거된다.
7. setTimeout 함수에 전달한 0ms 시간이 지난뒤 Callback으로 전달한 cb 함수가 Callback Queue에 추가된다. (이 부분은 다른예시와 함께 뒤에서 설명하겠다.)
8. Event Loop는 Call Stack이 비어있는 것을 확인하고 Callback Queue를 살펴본다. cb를 발견한 Event Loop는 Call Stack에 cb를 추가한다.
9. cb 함수가 실행 되고 내부의 console.log(‘second’)가 Call Stack에 추가된다.
10. console.log(‘second’)가 화면에 출력되고 Call Stack에서 제거된다.
11. cb가 Call Stack에서 제거된다.
위 일련의 흐름을 통해 JavaScript와 Browser의 구성요소들이 어떻게 동작하는지 이해할 수 있다.
Call Stack이 비어있을 경우, Callback queue에서 함수를 꺼내 Call Stack에 추가 합니다
Call Stack이 비어있을 경우. 이와 관련된 예제를 보겠습니다.
console.log('first');
setTimeout(
function cb() {
console.log('second');
}
, 0);
wait3Seconds();
console.log('third');
function wait3Seconds() {
let start = Date.now(), now = start;
while (now - start < 3 * 1000) {
now = Date.now();
}
}
위 코드의 핵심은 setTimeout과 7번 line의 wait3Seconds 함수다. line 2에서 setTimeout 함수에 delay 0을 주고 line 7에서 3초를 기다립니다. console.log 결과는 어떻게 될까...?
wait3Seconds 함수에 의해 3초 동안 console.log(‘third’)는 실행되지 않는다. 따라서 0ms 후에 실행된 console.log(‘second’)가 먼저 출력되지 않을까?
결과는 아래와 같다.
first
third
second
wait3Seconds 함수가 실행된 후 3초 동안 Call Stack 및 Event Loop 상태이다.
setTimeout이 호출된 후 0ms 뒤에 callback으로 전달 된 cb는 이미 Callback Queue 안에 있다. 그럼에도 불구하고 console.log(‘second’)가 third보다 뒤에 출력되는 이유는 Call Stack에 wait3Seconds함수가 있기 때문이다.
Event Loop는 Call Stack이 비어있지 않기 때문에(wait3Seconds)
Callback Queue를 체크하지 않는다. wait3Seconds 함수 종료 후엔 console.log(‘third’)가 Call Stack에 추가되어 먼저 출력된다.
위 예제를 통해 한 가지 확인할 수 있는 것은 setTimeout의 delay인자가 delay ms 후에 실행 되는 것을 보장하지 않는다는 것이다. 정확히는 delay ms 후에 Callback Queue에 들어가는 것을 보장한다.
Event Loop를 포함해 Browser의 구성요소 역할을 이해했다면, 자바스크립트 언어 자체가 비동기 특성을 제공하는게 아니라 Browser의 구성 요소들이 제공하는 것을 이해할 수 있어야 한다.
Call Stack은 코드가 동기적으로 실행될 때 쌓이는 곳이다. 자료 구조 중에 하나인 stack 형태로 쌓이는 게 특징이다.
Stack : 자료구조 중 하나, 선입후출(LIFO, Last In First Out)의 룰을 따른다.
Web API는 자바스크립트 엔진이 아니다. 브라우저에서 제공하는 API 로, DOM, Ajax, Timeout 등이 있다.
이러한 비동기 함수들은 각 함수마다 걸려진 조건들을 충족시키면, 콜백 큐로 이동한다.
좀 더 자세히 말하자면, Call Stack에서 실행된 비동기 함수는 Web API로 이동하고, Web API는 비동기 함수의 콜백함수를 Callback Queue에 밀어 넣는다.
ex) addEventListener( 'click' , function() {...})
addEventListener는 Web API로 들어가지만, 2nd 인자의 익명함수는 callback queue로 보내진다.
비동기적으로 실행된 콜백함수가 보관 되는 영역이다. 자료구조 Queue 형태로 쌓이는 게 특징이다.
Queue(큐) : 자료 구조 중 하나, 선입선출(FIFO, Frist In Frist OUT)의 룰을 따른다.
이 구조가 중요한 이유는 각각 들어가는 콜백 함수들의 종류가 다르고, 들어가는 큐에 따라서 이벤트 루프가 내보내는 우선 순위 역시 달라지기 때문이다.
Task Queue : 대표적으로 SetTImeout() 같은 타이머들이 여기에 들어간다.
Microtask Queue : 대표적으로 Promise의 then / catch , process.nextTick 가 여기에 들어간다.
Animation Frames : requestAnimationFrame API가 실행되면 콜백이 Animation Frames으로 담긴다.
브라우져마다 이벤트 루프의 탐색 순서가 다를 수 있다고는 하는데, 크롬 기준은 다음과 같다.
Microtask Queue => Animation Frames => Task Queue 순으로 실행된다.
마지막으로 문제 2문제만 풀어보겠다. 이 우선 순위를 기준으로 아래의 코드를 본다면, 어떤 순서로 실행 될까??
console.log("start");
setTimeout(function() {
console.log("this is from setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
});
requestAnimationFrame(function() {
console.log(" this is from requestAnimationFrame");
})
console.log("end");
결과 값
start
end
promise1
promise2
this is from requestAnimationFrame
this is from setTimeout
function oneMore(){
console.log('one more');
}
function run(){
console.log('func run is running');
setTimeout(()=> {
console.log('setTimeout is running');
}, 0);
new Promise((resolve) => {
// 여기까지는 동기이다.
resolve('hi');
// 그러나 then을 만나는 순간 비동기가 되는 거다.
} )
.then( console.log );
oneMore();
}
setTimeout( run, 5000 );
func run is running
one more
hi
setTimeout is running'