Front-end/깊게 파고들기

Javascript Event Loop 이벤트 루프 정리

아지송아지 2022. 4. 16. 12:06

안녕하세요

오늘은 javascript의 동작 원리, 이벤트 루프에 대해서 알아보겠습니다.

너무 어렵게 생각하지 않으시면 좋겠습니다.

 

 

 

단일 스레드


자바스크립트는 "단일 스레드"입니다. 처음 들으시는 분들은 어려운 단어입니다. 스레드가 하나라는 말은 동시에 하나의 작업만 처리할 수 있다는 뜻입니다. 하지만 자바스크립트로 개발해 보신 분은 아시겠지만 동시에 작업이 처리되는 것을 느끼셨을 겁니다. 이벤트가 일어날 때 다른 작업도 진행하고, 한 번에 여러 개의 HTTP 요청을 처리하기도 합니다.  어떻게 단일 스레드인데 이런 일이 가능할까요? 어떻게 동시에 여러 작업을 처리할 수 있는 걸까요? 바로 "이벤트 루프" 덕분입니다.

 

자바스크립트는 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원합니다.

아래 그림을 살펴보겠습니다.

Call Stack 이라는 부분이 있습니다. 코드가 실행되면 저곳에 쌓이게 되는데요 한 번에 여러 개를 실행시키는게 아닌 하나씩 실행시킵니다. 

 

여기서 짚고 넘어가야 할 점은 자바스크립트가 "단일 스레드"라는 말은 자바스크립트 엔진이 단일 호출 스택을 사용한다 라는 사실입니다.

자바스크립트가 구동되는 환경(브라우저, Node.js등)에서는 여러 개의 스레드가 사용됩니다. 이러한 구동 환경이 단일 호출 스택을 사용하는 자바스크립트 엔진과 상호 연동하기 위한 장치가 바로 "이벤트 루프"입니다.

 

 

 

Event Loop


이제 이벤트 루프를 좀 더 자세히 알아보겠습니다.

 

 

그림에는 몇 가지 용어들이 있습니다.

 

Heap

메모리가 할당이 되는 곳입니다.

선언한 변수, 함수가 담겨져 있습니다.

 

 

Call Stack

코드가 실행될때 쌓이는 곳입니다.  

단 하나의 호출 스택을 사용하기 때문에 자바스크립트의 함수가 실행되는 방식을 "Run to Completion"이라고 부릅니다.

이는 하나의 함수가 실행되면 이 함수가 끝날 때까지는 다른 작업은 끼어들지 못합니다.

 

요청이 들어올 때마다 순차적으로 호출 스택을 담아 처리합니다. 함수의 호출들은 "프레임" 스택을 형성한다고 말합니다. 함수가 실행되면 Call Stack에 새로운 프레임이 생기고 처리가 끝나면 없어지는 원리입니다.

 

function foo(a){
    const b = 2;
    return a + b;
}

function bar(x){
    const y = 1;
    return foo(x + y);
}

 const baz = bar(1); // 4는 baz에 할당된다.

위 코드의 동작 과정을 알아보겠습니다.

 

1. bar()가 호출될 때, 첫 번째 프레임이 생성되어 콜스택에 쌓입니다.

2. bar() 안에 있는 foo()가 호출될 때, 콜 스택에 첫 번째 프레임 위로 두 번째 프레임이 생성되어 쌓입니다.

3. foo()가 반환되면 두 번째 프레임은 없어집니다. (이제 콜스택에는 bar 프레임만 남아 있습니다.)

4. bar()가 반환되면 첫 번째 프레임이 없어져 콜스택은 이제 비어있습니다.

 

 

Web APIs

Web APIs는 자바스크립트 엔진 밖에 그려져있는 것을 확인하실 수 있습니다.

즉, 자바스크립트 엔진이 아닙니다. 이는 브라우저에서 제공하는 API로 비동기인 setTimeout, Promise 등이 있습니다. Call Stack에서 실행된 비동기 함수들은 모두 Web API를 호출합니다. 그리고 Web API는 콜백 함수를 Callback Queue에 넣습니다.

 

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0);
console.log(3);

위 코드 콘솔에 1 -> 3 -> 2 순서대로 출력됩니다. 동작 과정은 이러합니다.

 

1. console.log(1) 이 콜스택에 들어가 실행됩니다. (타고 들어가는 함수가 없으므로 바로 실행되어 사라집니다.)

2. setTimeout을 만나 콜스택은 이를 Web API로 보냅니다.

3. console.log(3) 이 콜스택에 들어가 실행됩니다. 

* Web API에 있는 setTimeout은 0ms 후에 해당 콜백을 Callback Queue에 넣습니다. 그리고 콜스택이 비워지면 Callback Queue에 있는 것을 콜스택에 가져와 console.log(2)를 실행시킵니다.

 

지금 첫 번째 그림을 보고 오시면 이해가 가실 겁니다.

중요한 점은 비동기 코드를 만나면 Web API 영역으로 빠지고 그 콜백은 바로 콜스택에 가는 것이 아닌 큐로 빠진다는 점입니다.

 

 

Callback Queue

앞서 Web API에서 설명드렸듯 비동기적으로 실행된 콜백 함수가 보관되는 곳입니다. 콜 스택에 가기 위한 "대기열"이라고 생각하시면 됩니다. 여기에 있는 콜백 함수들은 콜스택이 비어졌을 때 먼저 대기열에 들어온 순서대로 수행됩니다.

 

여기서 주의하셔야 할 점이 있습니다. 단순히 모든 비동기 코드가 이곳에 쌓이는것은 아닙니다.

Callback Queue에는 세 가지 종류가 있습니다.

1. Task Queue 

setTimeout, setInterval과 같은 코드입니다.

 

2. Microtask Queue

Promise callback, async callback과 같은 코드입니다.

 

3. Animation Frames

requestAnimationFrame과 같은 코드입니다.

 

"Microtask Queue > Animation Frame > Task Queue" 순으로  Microtask Queue가 가장 먼저 실행되고 Task Queue가 가장 늦게 실행됩니다.

 

지금부터는 gif 설명들이 많습니다.

https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax

위 주소에 더 자세한 내용이 있습니다.

 

그림과 같이 우선순위가 높은 큐가 비워져야만 다음 큐가 실행됩니다.

Microtask Queue가 비면 Task Queue가 실행되는 것이죠.

 

 

1. Start! 출력

 

2. setTimeout WEB API로 이동

 

3. setTimeout의 콜백은 Task Queue로 이동하고 Promise의 then이 Microtask Queue로 이동

 

4. End! 출력

 

5. Promise의 콜백이 콜스택에 들어가 res를 출력

 

6. Microtask Queue가 비어져 있으므로 Task Queue에 있는 setTimeout의 콜백에 콜스택으로 이동, Timeout! 출력

 

async와 awiat의 동작 과정까지 설명드리면 글이 너무 길어져서 다음 글에서 정리하겠습니다.

 

 

 

Event Loop

그렇다면 여러 그림들에서 이벤트 루프는 뭘하는 걸까요?

Call Stack과 Call Queue들을 감시하며 어떤게 비어져있고 어떤것을 채워야할지 정하며 수행합니다. 

while(queue.waitForMessage()){
  queue.processNextMessage();
}

대략 위와 같은 형태입니다.

현재 실행 중인 태스크가 없는지 태스크 큐에 태스크가 있는지 반복적으로 확인합니다. 

 

 

 

마치며


지금까지 자바스크립트의 이벤트 루프에 대해 알아보았습니다.

감사합니다.

 

 

참고 : 

https://asfirstalways.tistory.com/362

https://velog.io/@thms200/Event-Loop-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84

https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop

https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html

https://meetup.toast.com/posts/89

https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax