[JavaScript] 이벤트 루프 (feat. 자바스크립트 런타임)

Letmegooutside·2022년 1월 30일
0

JavaScript

목록 보기
22/25
post-thumbnail

자바스크립트 런타임

자바스크립트 런타임에는 setTimeout,DOM, AJAX 등과 같은 Web API(이는 JS엔진이 아니라 브라우저에서 제공된다)와 이러한 Web API의 호출을 통제하기 위한 이벤트 루프, 콜백 큐가 존재한다.

JS Engine

자바스크립트 엔진은 memory Heap과 Call Stack으로 구성되어 있다.
자바스크립트는 단일 스레드 프로그래밍 언어인데, 이는 Call Stack이 하나라는 의미이다.(한번에 하나의 일만 처리할 수 있다)

  • Memory Heap : 메모리 할당이 일어나는 곳. 우리가 프로그램에 선언한 변수, 함수 등이 담겨있다.
  • Call Stack : 함수의 호출을 저장하는 자료구조. stack 형태로 저장된다.

Call Stack

function first() {
  second();
  console.log('첫 번째');
}
function second() {
  third();
  console.log('두 번째');
}
function third() {
  console.log('세 번째');
}
first(); // fisrt 실행
third();

위의 코드의 실행 결과는 아래와 같다.

위 코드는 아래와 같이 콜스택에 쌓이고 실행되기 때문이다.(first가 실행되었을 때의 상황이다)

Web API

JS Engine이 아닌 브라우저에서 제공하는 APIsetTimeout,DOM, AJAX 등이 있다.
Call Stack에서 실행된 비동기 함수는 Web API를 호출하고, Web API는 콜백함수를 Callback Queue에 밀어 넣는다.

Callback Queue

비동기적으로 실행된 콜백함수가 보관되는 영역이다.
예를 들어 setTimeout에서 타이머 완료 후 실행되는 함수, addEventListener에서 클릭 이벤트가 발생했을 때 실행되는 함수 등이 보관된다.
태스크 큐, 마이크로 태스크, 잡 큐 등 여러 개로 구성되어 있다.

Event Loop

Call Stack과 Callback Queue의 상태를 체크하여 Call Stack이 빈 상태가 되면, Callback Queue의 첫번째 콜백을 Call Stack으로 밀어넣는다.
이러한 반복적인 행동을 틱(tick)이라고 부른다.

정리

  • JS엔진에서 코드가 실행되면, Call Stack에 쌓인다.
  • Stack의 구조에 따라 제일 마지막에 들어온 함수가 먼저 실행되며, Stack에 쌓여진 모든 함수가 실행된다.
    • 비동기 함수가 실행된다면, Web API가 호출된다.
    • Web API는 비동기 함수의 콜백 함수를 Callback Queue에 밀어 넣는다.
    • Event Loop는 Call Stack이 빈 상태가 되면, Callback Queue의 첫번째 함수를 Call Stack으로 이동시킨다.

자바스크립트는 단일 스레드 프로그래밍 언어라 한번에 하나씩 밖에 실행할 수 없지만, 이러한 런타임 환경 덕분에 멀티스레드 처럼 보여진다.

이벤트 루프 (Event Loop)

function run() {
  console.log('동작');
}
console.log('시작');
setTimeout(run, 3);
console.log('끝');

위 코드는 시작과 끝이 콘솔에 출력된 후 3초 후에 동작이 출력될 것이다.

이를 호출 스택으로 설명할 수 있을까?
먼저 console.log('시작')이 들어간다. 콘솔이 실행되어 호출스택에서 빠지고, 다시 setTimeout()이 들어간다. setTimeout()이 실행되어 빠지고, console.log('끝')이 들어간다.
이제 3초 후 run()이 실행되어야 할 차례에 호출 스택에는 run()이 존재하지 않는데 어떻게 실행된 것일까?
여기서 이벤트 루프와 Web API, Callback Queue가 등장한다.

setTimeout 3초의 경우 setTimeout이 호출되고 지워지면서 백그라운드로 run함수와 함께 3초타이머를 보내면, 백그라운드는 3초 후에 Callback Queue(태스크 큐)로 run함수를 보낸다.

이벤트 루프는 항상 대기하고 있다가, Call Stack이 비워지면 Callback Queue에서 함수를 하나씩 Call Stack으로 밀어올린다.

이제 run함수가 실행되고 호출스택에서 지워지게 된다.
이벤트 루프는 Callback Queue에 새로운 함수가 들어올 떄까지 대기한다.

function run() {
  console.log('동작');
}
console.log('시작');
setTimeout(run, 0);
console.log('끝');

위 코드를 살펴보면 setTimeout에 0초 타이머가 걸려있어서 바로 실행되어 시작 동작 끝의 순서로 콘솔에 출력될 것 같지만 일단 setTimeout을 하는 순간 백그라운드를 거쳐 Callback Queue로 run함수가 이동하기 때문에 시작 → 끝 → 동작 순서로 콘솔에 출력되게 된다.

그리고 백그라운드에서 3초를 정확하게 세어주었다고 해도, Call Stack에 함수들이 가득차있다면 정확히 3초후에 실행되지 않을 수도 있다. (이벤트 루프가 Callback Queue에서 콜백함수를 Call Stack으로 끌어올리지 못하기 때문이다)

Microtask Queue

Microtask Queue는 Promiseasync/await, process.nextTick, Object.observe, MutationObserver과 같은 비동기 호출을 넘겨받는다.

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');

위 코드는 아래와 같은 순서로 실행된다.

script start, script end, setTimeout 순으로 처리되는 것은 이해했는데 왜 Promise, setTimeout순으로 실행되는 것일까?

여기서 Microtask Queue의 개념이 나오는데, 이벤트 루프는 우선적으로 Microtask Queue를 확인한다.
만약 Microtask Queue에 콜백이 있다면 먼저 Call Stack에 담고, 더 이상 처리해야할 콜백이 없다면 그 때 Task Queue를 확인후 처리한다.

Promise.then()의 콜백은 Task Queue가 아닌 Microtask Queue에 담긴다.
따라서 위 코드는 Promise.then()의 콜백이 다 실행된 이후, setTimeout콜백이 실행되는 것이다.

Animation Frames

requestAnimationFrame API가 실행되면 콜백은 Animation Frames로 담긴다.

requestAnimationFrame(Function callback)

웹브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트(repaint)가 진행되기 전에 해당 애니메이션을 업데이트하는 지정된 함수를 호출한다.
requestAnimationFrame() 메소드는 비동기 함수로서 CSS 애니메이션으로는 처리가 어렵거나 canvas, SVG 등의 애니메이션을 직접 구현하고자 할 때 사용된다.

console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(function() {
  console.log("promise1");
}).then(function() {
  console.log("promise2");
});

requestAnimationFrame(function() {
    console.log("requestAnimationFrame");
})
console.log("script end");

위 코드의 실행결과는 아래와 같다.

Microtask Queue > Animation Frames > Task Queue 순으로 실행된다.
(크롬 기준. 브라우저마다 다를 수 있다.)

다시 정리

  • 코드가 실행되면 Call Stack에 쌓인다.
    • 비동기 함수가 실행되면, Web API가 호출된다.
    • Web API는 비동기 함수의 콜백 함수를 Callback Queue에 밀어넣는다.
      • Promise : Microtask Queue
      • timeout : Task Queue
      • requestAnimationFrame : Animation Frame
    • 이벤트 루프는 Call Stack이 빈 상태가 되면 콜백을 Call Stack으로 이동시킨다.
      • 이 때 콜백 이동의 우선순위는 Microtask Queue > Animation Frames > Task Queue 이다.



Reference
https://www.zerocho.com/category/JavaScript/post/597f34bbb428530018e8e6e2
https://velog.io/@thms200/Event-Loop-이벤트-루프
이벤트 루프의 동작을 시각적으로 확인할 수 있는 사이트

0개의 댓글