dev.toAlex Devero 님이 작성한 The JavaScript Event Loop Explained 번역 자료입니다. 번역에 문제가 있다면 댓글 달아주시구요. 원문을 보시기를 추천드립니다.

자바스크립트의 이벤트루프(event loop)는 이해해야 할 가장 중요한 것 중 하나입니다. 내부(under the hood)의 작동을 이해하는데 도움이 됩니다. 이 튜토리얼에서는 자바스크립트 이벤트 루프가 무엇이며 어떻게 작동하는지 배울 것입니다. 또한 콜스택(call stack), 웹API(web APIs) 와 메시지큐(message queue)에 대해 조금 배우게 됩니다.

자바스크립트의 빌딩 블록

자바스크립트에는 몇 가지 기본적인 빌딩 블록이 있습니다. 이러한 블록은 메모리 힙(memory heap) , 스택(stack) , 콜스택(call stack), 웹API(web APIs), 메시지큐(message queue)와 이벤트루프(event loop)입니다. 메모리힙은 자바스크립트가 객체와 함수를 저장하는 곳입니다. 스택은 원시 데이터 타입(primitive data types)값과 같은 정적 데이터(static data)를 저장하기 위한 것입니다.

콜스택은 자바스크립트가 실행해야 하는 기능을 추적하는데 사용하는 메커니즘입니다. 웹API는 웹 브라우저에 내장된 API입니다. 이러한 API를 사용하면 일반적인 방법으로는 할 수 없는 기능을 사용할 수 있습니다. 예를들어 fetch API, Geolocation API, WebGL API, Web Workers API등이 있습니다.

이런 API는 자바스크립트 언어의 일부가 아닙니다. 핵심 자바스크립트 언어 위에 구축된 인터페이스입니다. 이것이 모든 자바스크립트 환경에서 사용할 수 없는 이유이기도 합니다. 웹API는 setTimeout 또는 이벤트와 같은 비동기메서드(async methods)도 취급합니다. 이제 메시지큐와 이벤트루프에 대해 알아보겠습니다.

메시지큐

메시지큐는 기본적으로 저장소입니다. 자바스크립트가 처리해야 하는 “메시지(messages)”를 보관하는 곳입니다. 이러한 메시지는 기본적으로 setTimeout과 같은 비동기 함수(async functions)와 함께 사용되는 콜백 함수이며 유저가 트리거하는 이벤트이기도 합니다. 예를 들어 클릭과 키보드 이벤트가 있습니다.

이러한 비동기 함수가 실행되거나 이벤트가 발생하면 자바스크립트는 먼저 해당 함수를 콜스택으로 보냅니다. 자바스크립트는 각 함수나 이벤트를 적절한 웹API로 보내 처리합니다. API가 필요한 작업을 수행하면 연결된 콜백 함수가 있는 메시지를 메시지큐로 보냅니다.

이러한 메시지는 콜스택이 비워질 때까지 메시지큐에 저장됩니다. 콜스택이 비면 큐의 첫 번째 메시지인 콜백이 콜스택으로 푸시됩니다. 콜스택은 해당 콜백과 여기에 포함된 코드를 실행합니다.

메시지큐에 대해 한 가지 중요한 것이 있습니다. 콜스택은 LIFO 원칙을 따릅니다. 콜스택에 마지막으로 푸시된 함수가 첫 번째 함수로 처리됨을 의미합니다. 메시지큐는 이 원칙을 따르지 않습니다. 메시지큐의 경우 첫 번째 메시지나 콜백이 처리됩니다.

메시지큐 작동 방식의 간단한 예

setTimeout메서드를 시연해 보겠습니다. setTimeout메소드를 사용하면 자바스크립트가 이를 실행할 콜스택으로 보냅니다. 실행하면 새 타이머가 생성됩니다. 이 타이머는 적절한 웹API로 전송됩니다. 그러면 이 API가 카운트다운을 시작합니다.

카운트다운이 0에 도달하면 API는 setTimeout메서드의 콜백을 메시지큐로 보냅니다. 콜백은 콜스택이 비워질 때까지 메시지큐에서 기다립니다. 콜스택이 비어 있으면 자바스크립트는 메시지큐에서 콜백을 가져와 콜스택으로 푸시한 다음 실행합니다.

// Use setTimeout method to delay
// execution of some function
setTimeout(function cb() {
  console.log('Hello.')
}, 500)

// Step 1:
// Add to call stack: setTimeout(function cb() { console.log('Hello.') }, 500)

// Call stack                                         //
// setTimeout(function cb() { console.log('Hello.') } //
//                                                    //

// Step 2:
// Send cb() to web API
// and remove setTimeout from call stack
// and create timer: 500

// Call stack //
//            //
//            //

// web API     //
// timer, cb() //
//             //

// Step 3:
// When timer is up, send cb() to message queue
// and remove it from web API

// web API     //
//             //
//             //

// message queue //
// cb()          //
//               //

// Step 4:
// When call stack is empty, send cb() to call stack
// and remove it from message queue

// message queue //
//               //
//               //

// Call stack //
// cb()       //
//            //

콜스택, 메시지큐와 우선 순위

자바스크립트에서 콜스택과 메시지큐는 다른 우선 순위를 갖습니다. 콜스택의 우선 순위가 메시지큐의 우선 순위보다 높습니다. 결과적으로 메시지큐는 큐에서 콜스택으로 무엇이든 푸시하기 전에 콜스택이 비워지기를 기다려야 합니다.

콜스택이 비어 있는 경우에만 메시지큐의 첫 번째 메시지나 콜백을 푸시할 수 있습니다. 언제 이런 상황 발생하나요? 콜스택은 내부의 모든 함수 호출과 이러한 호출의 콜스택이 실행될 때 비어 있게 됩니다. 이 경우 콜스택이 비어 메시지큐에 사용할 수 있습니다.

메시지큐 처리와 제로지연(zero delays)

메시지큐는 한 번에 하나의 메시지만 처리할 수 있습니다. 또한 메시지큐에 여러 메시지가 포함된 경우 다른 메시지가 처리되기 전에 각 메시지를 먼저 처리해야 합니다. 모든 메시지의 처리는 이전 메시지의 완료 여부에 따라 달라집니다. 한 메시지가 처리되는데 시간이 더 걸리면 다른 메시지는 기다려야 합니다.

이 원칙을 run-to-completion이라고 합니다. 이것은 제로지연(zero delays)이라고 하는 또 다른 의미가 있습니다. setTimeout메소드를 사용 하고 지연(delay)을 0으로 설정 한다고 가정해 보겠습니다. 이 아이디어는 이 timeout에 전달된 콜백이 즉시 실행되어야 한다는 것입니다. 현실은 그렇지 않을 수도 있습니다.

아시다시피 메시지큐는 한 번에 하나의 메시지만 처리할 수 있습니다. 큐에서 다른 메시지를 처리하기 전에 각 메시지가 완료되어야 합니다. 따라서 setTimeout지연을 0으로 설정하여 사용 하면 메시지큐의 첫 번째 메시지인 경우에만 콜백이 즉시 실행됩니다. 그렇지 않으면 기다려야 합니다.

자바스크립트 이벤트루프

이것이 자바스크립트가 비동기 작업을 처리하는 방법입니다. 이것이 콜스택, 웹API와 메시지큐 간에 작업이 전달되는 방식입니다. 자바스크립트 자체가 싱글 스레드이지만 웹API가 별도의 스레드에서 실행되기 때문에 이를 수행할 수 있습니다. 자바스크립트 이벤트루프가 이것과 무슨 관련이 있나요?

이 사이클을 처리하는 것은 자바스크립트 이벤트루프입니다. 콜스택이 비어 있는지 여부를 지속적으로 확인하는 것이 자바스크립트 이벤트루프의 작업입니다. 비어 있으면 메시지큐에서 첫 번째 메시지를 가져와 콜스택으로 푸시합니다.

콜스택이 비어 있지 않은 경우 이벤트루프는 큐의 메시지를 허용하지 않습니다. 대신 콜스택 프로세스가 내부에서 호출하도록 합니다. 이벤트루프의 이러한 각 사이클 또는 반복을 “tick”이라고 합니다.

프라미즈(promises)와 비동기 함수(async functions)에 대한 참고 사항

setTimeout또는 이벤트와 같은 비동기 메서드는 웹API와 메시지큐에서 처리됩니다. 이것은 비동기 함수(async functions)프라미즈에는 적용되지 않습니다. 비동기 함수와 프라미즈는 다른 큐에서 처리됩니다. 이 큐을 작업 (job queue)이라고 합니다. 이 큐의 또 다른 이름은 마이크로태스크 큐(microtask queue)입니다.

따라서 프라미즈나 비동기 함수를 사용하면 setTimeout이 다르게 처리됩니다. 첫째, 프라미즈와 비동기 함수는 작업큐에서 처리됩니다. setTimeout은 메시지큐에 의해 처리됩니다. 둘째, 작업큐는 메시지큐보다 우선 순위가 높습니다. 이것은 한 가지 중요한 의미를 가지고 있습니다.

프라미즈와 setTimeout이 있다고 가정해 봅시다. 프라미즈는 즉시 해결되고 setTimeout은 지연이 0으로 설정됩니다. 따라서 +/-도 즉시 실행되어야 합니다. 이것을 더 흥미롭게 만들기 위해 다른 일반 함수도 추가해 보겠습니다. 이 함수는 마지막에 있습니다. 결과가 어떻게 될까요?

가장 먼저 실행될 함수는 마지막에 넣은 일반 함수가 될 것입니다. 다음으로 프라미즈의 모든 콜백이 실행됩니다. setTimeout에 대한 콜백은 마지막으로 실행됩니다. setTimeout메서드가 코드의 프라미즈 위에 배치되는 것은 중요하지 않습니다.

중요한 것은 작업큐가 메시지큐보다 우선 순위가 높다는 것입니다. 결과적으로 프라미즈와 setTimeout이 경합할 때 승자는 프라미즈입니다.

// Create a function
function myFuncOne() {
  console.log('myFuncOne in setTimeout.')
}

// Create another function
function myFuncTwo() {
  console.log('myFuncTwo after the promise.')
}

// Delay the myFuncOne() by 0 seconds
setTimeout(myFuncOne, 0)

// Create a promise and resolve it immediately
new Promise((resolve, reject) => {
  resolve('Message from a promise')
})
  .then(res => console.log(res))

// Call the myFuncTwo()
myFuncTwo()

// Output:
// 'myFuncTwo after the promise.'
// 'Message from a promise'
// 'myFuncOne in setTimeout.'

결론: 자바스크립트 이벤트 루프 설명

자바스크립트 이벤트 루프를 이해하면 자바스크립트 내부의 작동을 이해하는데 도움이 됩니다. 이를 이해하려면 콜스택, 웹API와 메시지큐과 같은 주제도 이해해야 합니다. 이 튜토리얼이 이러한 주제와 중요한 자바스크립트 이벤트 루프를 이해하는데 도움이 되었기를 바랍니다.