Javascript 동작 원리
Call stack
function first() {
second();
console.log('first');
}
function second() {
third();
console.log('second');
}
function third() {
console.log('third');
}
first();
third
second
first
jacascript에서 일반적인 함수의 (depth가 있는)호출은 stack구조로 실행되며 함수의 execution context가 stack에 쌓인다. (동일 depth에 first, second, third가 있다면 first push, first pop, second push, second pop, third push, third pop 순으로 동작)
콜백 처리 (Call stack, Event Loop, Task Queue)
그렇다면 callback(setTimeout 등에 의한)함수의 호출은 어떤 순서로 실행될까?
우선 짚고 넘어가야할 점은 javascript는 python과 같은 single thread 언어라는 것이다. 사용자 입장에서 얼핏보면 main stream과 callback stream이 병렬(parallelism)로 실행되는 것처럼 보일지라도 사실은 동시(concurrency)에 실행될 뿐이다. parallelism이란 실제로 여러개의 core에 의해 같은 시간에 여러 작업이 이루어지는 것이고, concurrency란 1개의 core에 의해 여러개가 번갈아서 함께 진행되는 것이다.
참고로 python은 default로 5ms 마다 thread의 switching이 발생한다고 한다. 떄문에 concurrency이더라도 race condition이 발생할 수 있는 것!
javascript는 concurrency는 태스크큐와 이벤트루프에 의해 동작한다.
javascript engine은 callstack에 있는 실행 컨텍스트를 실행한다. Settimeout이나 http request 같은 비동기 명령(javascript의 기능이 아니고 browser가 제공하는 함수)이 실행되면 그 비동기 명령 자체가 call stack에 올라왔다가 Web API를 처리하는 영역으로 옮겨진다. setTimeout의 경우라면 입력된 시간 뒤에, http request라면 response가 왔을 때 callback만을 task queue로 밀어 넣는다. 이 queue에 있는 callback들은 call stack이 비었을 때 event loop에 의해 call stack으로 옮겨지고 실행되게 된다.
(추가로, event loop은 user의 click이나 등록된 다른 event에 대한 처리도 담당한다.)
무한 루프를 돌며 로그를 찍는 함수 func1을 setTimeout(0)의 callback으로 요청하고 또 다른 무한 로그를 찍는 함수 func2를 실행했을 때, func1가 영원히 실행되지 못하는 이유는 func1가 task queue에 있지만 func2가 call stack을 비우지 않기 때문이다.
Microtask Queue
Microtask Queue라는 개념을 알게되어 추가한다.
위에서 말한 task queue말고도 microtask queue라는 것이 존재하며 이는 Promise의 동작과 관련이 있다.
(Macro)Task Q vs Microtask Q
callback 함수가 들어가는 점은 같지만 어떤 함수에 의해 어떤 Q로 들어가는지, Q의 실행 우선순위 등이 다르다.
어떤 함수들이 어떤 큐에 callback을 넣는가?
- Task queue: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, IO rendering
- Microtask queue: process.nextTick, Promise, Object.observe, MutationObserver
Event loop은 어떤 순서로 task를 꺼내오는가?
- callstack이 비었을 때, microtask queue의 task를 전부 실행한 후 (mcaro)task queue의 task 하나를 실행한다. 이를 반복함
Ex 1)
setTimeout(() => {
console.log('task queue');
}, 0);
const promise = Promise.resolve('Promise');
promise.then(() => console.log('microtask queue'));
console.log('call stack');
// call stack
// microtask queue
// task queue
- call stack에서 setTimeout script 실행하여 callback과 time을 web api로 보냄 (0초가 지난 후 callback이 task queue로)
- call stack에서 promise를 생성한다. new Promise 내부의 함수는 일반적인 function call로 실행된다.(위의 경우는 바로 resolve만 하기 떄문에 promise 내부 함수 없음)
- .then()에 의해 callback이 등록된다. 등록된 callback은 promise가 resolve될 때 microtask queue로 이동된다. (위의 예제에서는 바로 resolve됐기 때문에 바로 이동 함)
- call stack에서 console.log를 실행하여 'call stack'을 출력한다.
- call stack이 비며 event loop가 microtask 큐에 있는 모든 task를 순차적(오래된 순)으로 call stack에 올려 실행 하여 'microtask queue'를 출력한다.
- microtask 큐가 비며 event loop가 task 큐에 있는 가장 오래된 하나의 task를 꺼내 call stack에 올려 실행한다('task queue' 출력).
Ex 2)
const myPromise = () => Promise.resolve('promise resolved');
// const myPromise = () =>
// new Promise((resolve) => {
// resolve('promise resolved');
// });
const myFunc = async () => {
console.log('Before inner function');
console.log(await myPromise());
console.log('After inner function');
};
// const myFunc = () =>
// new Promise(() => {
// console.log('Before inner function');
// myPromise().then((v) => {
// console.log(v);
// console.log('After inner function');
// });
// });
console.log('Before Function');
myFunc();
console.log('After Function');
// Before Function
// Before inner function
// After Function
// promise resolved
// After inner function
- console.log('Before function')에 call stack에 들어갔다가 실행되고 나온다.
- myFunc()가 call stack에 쌓이고 실행된다.
- console.log('Before inner function')이 call stack에 들어갔다가 실행되고 나온다.
- myPromise()가 call stack에 들어갔다가 실행되고 나온다.(promise를 return하면서 promise의 내부함수도 실행하지만 이 경우엔 없으므로 패스)
- await에 의해 myFunc의 해당 라인부터 나머지가 myPromise의 callback으로 등록된다.
- myPromise가 resolve되면(이미 돼있으니 바로) microtask queue에 callback을 넣는다.
- myFunc이 callstack에서 pop된다.
- console.log('After function')에 call stack에 들어갔다가 실행되고 나온다.
- call stack이 비어 microtask queue에서 callback을 call stack에 가져와서 실행시킨다. (myFunc() 뒷부분 실행됨)