[JS] Event Loop 동작 구조

나나콘·2024년 5월 6일

Browser의 Multi Thread

JavaScript : Single Thread 언어

웹 애플리케이션에서는 네트워크 요청 이벤트 처리 타이머와 같이 멀티로 처리해야 하는 경우가 많다.

Browser 내부의 Multi Thread인 Web APIs에서 비동기 + 논블로킹으로 처리된다.
비동기 + 논블로킹은 Main Thread가 작업을 다른 곳에 요청하여 대신 실행하고, 그 작업이 완료되면 Event나 Callback 함수를 받아 결과를 실행하는 방식을 말한다.

Event Loop

브라우저의 동작 타이밍을 제어하는 관리자

Browser 내부 구성도

구성 요소 :

  • Web APIs : 브라우저에서 제공하는 API 모음, 비동기적으로 실행되는 작업들을 전담하여 처리
  • Event Table :
  • Callback Queue : 비동기적 작업이 오나료되면 실행되는 함수들이 대기하는 공간
  • Call Stack : Javascript 엔진이 코드 실행을 위해 사용하는 메모리 구조
  • Event Loop : 비동기 함수들을 적절한 시점에 실행시키는 관리자

Web APIs 종류

  • Dom (document) : HTML 문서의 구조와 내용을 표현하고 조작할 수 있는 객체
  • XMLHttpRequest (ajax) : 서버와 비동기적으로 데이터를 교환할 수 있는 객체. AJAX기술의 핵심.
  • Timer API (setTimeout): 일정한 시간 간격으로 함수를 실행하거나 지연시키는 메소드들을 제공

Callback Queue의 종류

  1. (Macro)Task Queue
    : setTimeout, setInterval, fetch, addEventListener와 같이 비동기로 처리되는 함수들의 callback 함수가 들어가는 Queue
  2. MicroTask Queue (처리 우선순위가 높음)
    : promise.then, process.nextTick, MutationObserver와 같이 우선적으로 비동기로 처리되는 함수들의 callback 함수가 들어가는 Queue

가장 우선순위가 높은 microtask queue를 먼저 처리하여 비우고 그 다음 task queue의 콜백을 처리한다.

ex) Promise.thensetTimeout중 무엇이 먼저 실행될까?

Promise.then은 microtask queue
setTimeout은 macrotask queue
=> Promise.then이 먼저 실행된다.


Javascript Event Loop 동작 과정

Event Loop는 비동기 함수 작업을 Web API에 옮기는 역할을 하고 작업이 완료되면 Callback Queue에 적재했다가 다시 JavaScript 엔진에 적재해 수행시키는 일종의 작업을 옮기는 역할만 한다.

작업을 처리하는 주체는 Javascript 엔진Web API다.

그래서 Event Loop
1. Call Stack에 현재 실행중인 작업이 있는지
2. Task Queue에 대기중인 작업이 있는지
반복적으로 확인하는 일종의 무한루프만 돌고,
대기 작업이 있따면 작업을 옮겨주는 형태로 동작한다.


Promise 내부 동작 과정

MicroTask Queue 처리 과정

console.log('Start!'); // 1

setTimeout(() => { // 3
	console.log('Timeout!');
}, 0);

Promise.resolve('Promise!').then(res => console.log(res)); // 4

console.log('End!'); // 2

실행 과정

  1. Call Stack에 console.log('Start!'); 적재되고 실행되어 출력한다.
  2. setTimeout 코드가 Call Stack에 적재되고 실행되면, () => { console.log('Timeout!') }이 Event Loop에 의해 Web API로 옮겨지고 타이머가 작동한다.
  3. 타이머가 종료되고 setTimeout의 callback 함수는 Event Loop에 의해 MacroTask Queue에 적재한다.
  4. Promise 코드가 Call Stack에 적재되어 실행되고, then 핸들러 callback 함수가 Event Loop에 의해 MicroTask Queue에 적재한다.
  5. console.log('End') 코드가 실행되어 출력한다.
  6. Main Thread의 JS 코드가 실행되어 더이상 Call Stack이 비워진다.
  7. Callback Queue에 남아있는 Callback 함수들을 빼와 Call Stack에 적재한다.
  8. Callback Queue 중에서 우선순위가 높은 MicroTask Queue에 남아있는 callback이 우선적으로 처리된다.
  9. MicroTask Queue가 비어지면, MacroTask Queue에 있는 callback 함수를 Call Stack에 적재해 실행한다.

출력 결과

Start!
End!
Promise!
Timeout!

Async/Await 내부 동작 과정

Async/Await는 비동기 논블로킹 동작을 동기적으로 처리하기 위해 복잡한 콜백이나 then 핸들러의 지옥 코드를 극복하는 핵심이다.
Async/Await는 단순히 비동기를 동기적으로 처리해준다는 효과만 알고있어서, 비동기 코드와 동기 코드가 같이 쓰여져 있을 경우 이들의 확실한 처리 과정을 알아보자

async 함수를 동일 코드 레벨에서 실행해보자.

const one = () => Promise.resolve('One!');

async function myFunc(){
	console.log('In function!');
	const res = await One();
	console.log(res);
}

console.log('Before Function!');
myFunc();
console.log('After Function!');

실행 과정

  1. Before Function이 출력된다.
  2. async 함수인 MyFunc()이 호출된다.
  3. async 함수 안에 있는 console 함수가 실행되어 console에 'In Function!'이 출력된다.
  4. Promise 객체를 반환하는 one() 비동기 함수를 호출한다.
  5. 이때 one() 비동기 함수 왼쪽에 await 키워드로 인해, MyFunc 함수의 내부 실행은 잠시 중단되고 Call Stack에서 빠져나와 나머지 부분은 MicroTask Queue에 적재된다.
    이는 Javascript 엔진이 await 키워드를 인식하면 async 함수의 실행은 지연되는 것으로 처리하기 때문이다.
  6. 'After Function'이 출력된다.
  7. Call Stack이 모두 비어서 MicroTask Queue에 있는 async 함수를 빼와 Call Stack에 적재한다.
  8. Promise 객체의 결과물인 'One!' 문자열을 변수 res에 받고 이를 console에 출력한다.

실행 결과

Before function!
In function!
After function!
One!

Async/Await 오해와 진짜 동작

이번에는 MyFunc() 함수를 호출하는 Main 스택에서 await 키워드를 붙여주면 어떻게 될까?

const one = () => Promise.resolve('One!');

async function myFunc(){
	console.log('In function!');
	const res = await one();
	console.log(res);
}

console.log('Before Function!');
await myFunc(); // await를 추가했다.
console.log('After Function!');

출력 결과

Before Function!
In function!
One!
After Function!

이전 결과와 다르다.

const one = () => Promise.resolve('One!');

async function myFunc(){
	console.log('In function!');
	const res = await one();
	console.log(res);
}

console.log('Before Function!');
await myFunc();
console.log('After Function!');

/* ---------------- ↓↓↓ 변환 ↓↓↓ ---------------- */

const one = () => Promise.resolve('One!'); 

function myFunc(){
	console.log('In function!');
	return one().then(res => { // await -> then
	  console.log(res);
	});
}

console.log('Before Function!');
myFunc().then(() => { // await -> then
  console.log('After Function!');
});

즉, await myFunc() 다음에 나오는 코드들이 myFunc()의 then 핸들러의 콜백으로서 Microtask Queue에 적재되고 이벤트 루프에 다시 Call stack에 옮겨져서 실행되는 것이다.


📘 References

[Inpa Dev 👨‍💻:티스토리] 🔄 자바스크립트 이벤트 루프 동작 구조 & 원리 끝판왕

profile
지식을 기록하고, 경험을 코드로 남겨라.

0개의 댓글