자바스크립트 이벤트 루프
자바스크립트는 싱글쓰레드 기반의 언어입니다. 싱글 쓰레드 기반이라는 이야기는, 동시에 하나의 작업만을 처리할 수 있다는 뜻이 됩니다. 그런데 자바스크립트로 개발을 진행하면 여러가지 작업들이 동시에 진행되고 있음을 확인할 수 있습니다. 예를 들어, 마우스 입력을 처리하면서 애니메이션 효과를 동시에 보여주기도 합니다. 이런 부분이 어떻게 가능한 것일까요?
싱글 스레드와 멀티 스레드
스레드는 프로세스가 할당받은 자원을 이용하는 실행의 단위입니다. 한 프로세스 내에서 동작되는 여러 실행의 흐름들을 프로세스의 구성요소인 Heap, Data, Code
영역과 공유합니다.
싱글 스레드는 이러한 스레드가 하나 존재하는지, 여러개가 존재하는지를 뜻합니다. 싱글 스레드라면, 하나의 프로세스 내에 하나의 스레드만 존재하므로 하나의 스택만이 존재한다고 할 수 있습니다. 반면, 멀티 스레드는 프로세스 내에 여러개의 스레드가 병렬적으로 작업을 처리할 수 있습니다.
여기서 자바스크립트는 싱글 쓰레드 언어임을 다시 확인해보면, 자바스크립트는 분명히 한번에 하나의 작업만을 진행하는 것이 알맞아 보입니다.
( 출처 : https://velog.io/@eunjin/OS-싱글스레드-멀티스레드의-의미 )
Run to completion
위와 같은 자바스크립트의 특징으로, 자바스크립트는 하나의 함수가 실행되면 해당함수의 실행이 끝날때 까지 다른 작업은 실행되지도, 끼어들지도 못합니다. 이러한 부분은, 실행한 함수가 다른 작업에 의해 선점될 일이 없고 다른 모든 코드의 실행보다 우선해서 값을 변경할 수 있으며 중단되지 않으므로 프로그램의 동작을 유추할 때에 도움이 될 수 있습니다.
다만, 이러한 특징의 문제점은 하나의 함수를 실행하동안 해당 함수를 처리하는 동안 다른 작업의 실행이 중단되는 단점을 가지게 됩니다.
function delay() { for (var i = 0; i < 100000; i++); } function foo() { delay(); bar(); console.log('foo!'); // (3) } function bar() { delay(); console.log('bar!'); // (2) } function baz() { console.log('baz!'); // (4) } setTimeout(baz, 10); // (1) foo();
위와 같은 예시의 상황을 살펴보면, 다음과 같이 진행됩니다.
- setTimeout 함수가 실행되었고 콜스택에서 제거됩니다.
- setTimeout의 콜백함수가 실행되기 전에,
foo
함수가 실행됩니다. foo
함수는delay, bar
를 실행합니다.bar
함수의console.log('bar')
가 실행되고bar
함수는 콜스택에서 제거됩니다.- 콜스택 최상단에 있는
foo
함수의cosnole.log('foo')
가 실핼되고 콜스택에서 제거됩니다. - setTimeout 함수의 콜백함수인
baz
함수가 실행됩니다.
이러한 과정을 살펴 볼때, 우리는 함수의 실행이 중단되는 부분을 확인할 수 있습니다.
원래, setTimeout
함수의 콜백함수인 baz
는 10ms
이후에 실행되어야 합니다. 하지만, 이후 콜스택에 다른 함수들이 들어갔고 해당 함수들의 처리가 완료되기 전까지 baz
함수는 실행되지 않습니다. 따라서, 10ms
라는 시간 또한 보장되지 않습니다.
그런데, 이 foo
함수가 완료되고 난 후 baz
함수는 어떻게 이어서 실행될 수 있었을까요? baz
함수는 foo
함수에서 실행한 함수가 아닙니다. 이미 콜스택에서 제거된 setTimeout
의 콜백함수입니다. 이때 등장하는 것이 이벤트 루프입니다.
이벤트 루프
while(queue.waitForMessage()){ queue.processNextMessage(); }
MDN
의 문서를 따르면 이벤트 루프는 대략 위와 같은 형태로 이루어져 있습니다. 이벤트 루프는 현재 실행중인 태스크가 존재하는지(콜스택이 비어져 있는지), 태스크 큐에 태스크가 남아 있는지를 반복적으로 확인합니다. 그리고 현재 태스크 큐에 남아 있는 함수를 자바스크립트의 콜스택에 옮겨 함수가 바로 실행될 수 있도록 합니다. 이러한 이벤트 루프를 통해 위의 예제에서의 baz
함수는 foo
가 완료된 후 바로 실행될 수 있습니다.
그런데, 위의 그림을 보게 되면 Web Api
가 등장합니다.
웹 API와 이벤트루프
웹 API
는 자바스크립트가 실행되는 런타임 환경에 존재하는 별도의 API이며, 브라우저가 지원합니다. 이 웹 API
의 종류에는 DOM, setTimeout, fetch
등이 있습니다. 이러한 웹 API는 브라우저에서 실행되는 별도의 API이므로 자바스크립트에서 처리하지 않습니다. 다만, 이때의 콜백 함수는 자바스크립트의 콜스택에서 처리하게 됩니다. 다음 예제를 통해 이를 확인할 수 있습니다.
console.log("1"); setTimeout(() => { console.log("2"); }, 1000); console.log("3");
- 자바스크립트 엔진은
console.log('1')
를 만나 처리합니다. setTimeout
을 만나면,setTimeout
함수를 웹 API 가 처리하고1000ms
동안 대기합니다.- 위의
setTimeout
이 대기하는 중,console.log('3')
이 처리됩니다. 웹 API
는1000ms
가 지난 후callback queue
로console.log('2')
를 보냅니다.
→ 이 과정에서 이벤트 루프는 콜백 큐와 콜스택을 계속해서 확인하는 중입니다.
callback queue
에 새로운 함수가 들어와 있고, 콜스택이 비어져 있으므로 콜스택에 있는cosnole.log('2')
를 콜스택으로 옮깁니다.console.log('2')
가 실행됩니다.
console.log("1"); setTimeout(() => { console.log("2"); }, 0); console.log("3"); // -------- 시간이 0이더라도 똑같습니다. 위와 똑같은 과정을 거치게 되니까요.
태스크 큐와 마이크로 태스크 큐
그런데, 위의 callback queue
는 태스크 큐, 마이크로 태스크 큐로 나누어 볼 수 있습니다. 이 각각 의 큐는 다음과 같은 역할을 합니다.
- 태스크 큐 :
setTimeout, setInterval
의 콜백함수를 처리합니다. - 마이크로 태스크 큐:
promise, async
의 콜백함수를 처리합니다.
여기서 중요한 부분은 마이크로 태스크 큐의 우선도가 태스크 큐보다 높다는 점입니다.
setTimeout(function () { // (A) console.log("A"); }, 0); Promise.resolve() .then(function () { // (B) console.log("B"); }) .then(function () { // (C) console.log("C"); });
위의 예제에서 setTimeout
은 태스크 큐가, Promise
는 마이크로 태스크 큐가 처리합니다. 그런데, 이를 실행해 보면 B -> C -> A
순으로 실행됩니다.
만약, 우선도가 같거나 큐가 하나였다면 wep api
가 해당 함수를 실행하고 큐에 콜백함수를 순서대로 넣었을 것이고 큐는 선입선출로 실행되므로 setTimeout
이 먼저 실행되었어야 할 것입니다. 하지만, 마이크로 태스크 큐가 태스크 큐보다 우선도를 가지게 되므로 promise
가 먼저 처리되게 됩니다.