JS 이벤트 루프

불불이·2024년 2월 3일
0

다시한번 복습하기 위해 정리를 한다.

이 글은 모두Inpa Dev 👨‍💻:티스토리에 작성된 글을 다시 한번 작성하고 개인적으로 정리한 내용이니 공부를 하실거면 위의 링크에서 보시는게 훨씬 도움이 되실겁니다!

js는 싱글스레드임에도 불구하고 오래 걸리는 작업이 가능하다. 그 이유는 브라우저 내부 멀티스레드인 Web APIs에서 그 작업을 처리하기 때문이다.
(Node.j에서는 libuv 내장 라이브러리가 처리한다.)

Event Loop: 브라우저 동작을 제어하는 관리자
1. 비동기 자바스크립트 코드를 브라우저 Web APIs에게 맡김
2. 백그라운드 작업이 끝난 결과를 콜백 함수 형태로 Callback Queue에 넣고 처리 준비가 되면
3. Call Stack(호출 스택)에 넣어 마무리 작업을 한다.

한가지 도움이 됐던 것은 이름을 그냥 콜스택, 콜백큐 라고 아무 생각 없이 적는 것 보다는 Call Stack 호출할 스택 이라던가 Callback Queue 비동기 작업이 완료되면 실행되는 함수들이 대기하는 공간 이런식으로 풀어서 생각하는게 도움이 되었다.

Event Loop도 마찬가지이다.
Event Loop라는 이름이 붙여진 이유는 마치 순회하는 것 처럼 작업이 이루어져서다.

Web APIs

Web APIs는 위에 언급했듯이 오래걸리는 작업을 처리하는 부분이다. 오래 걸리는 작업에도 종류도 여러가지로 나눌 수 있다.

  1. DOM
  2. XMLHttpRequest
  3. Timer API
  4. Console API
  5. Canvas API
  6. Geolocation API

이 Web API들이 모두 비동기적으로 처리되는 것이 아니다.
DOM이나 Console은 동기적으로 처리된다.

Callback Queue

콜백 큐에도 종류가 있다.
1. Task Queue: setTime, setInterval, fetch 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐
2. Microtask Queue : Promise.then, process.nextTick과 같이 우선적으로 비동기가 처리되는 함수들의 콜백 함수가 들어가는 큐
3. Animation Frames: 브라우저 애니메이션 관련

우선 순위
1. Microtask Queue
2. Task Queue

큐에도 우선순위가 있는지 처음 알았다..🧐

이벤트 루프를 배우기 전에 Promise.then 결과가 setTimeout보다 우선 된다는 것을 미리 배웠다면, 왜 프로미스가 먼저 처리되는지에 대한 이유가 이벤트 루프의 동작 원리와 관련이 있다는걸 알 수 있을 것이다.

Promise MicroTask Queue 처리 과정

console.log('Start!');

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

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

console.log('End!');
  1. Start 출력
  2. setTimeout() 함수가 실행 되지만 Task queue로 들어감
  3. Promise는 Microtask queue로 들어감
  4. End 출력
  5. 우선순위에 따라 Promise 먼저 출력
  6. Timeout 출력

결과

> Start!
> End!
> Promise!
> Timeout!

Async/Await 내부 동작 과정

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. myFunc() 실행
  3. In function! 출력
  4. One() 비동기 함수 왼쪽에 await 키워드로 인해, myFunc()함수의 내부 실행은 잠시 중단되고 Call stack에서 빠져나와 나머지 부분 microtask queue에 적재.
    js 엔진이 await 키워드를 인식하면 async 함수의 실행은 지연되는 것으로 처리
  5. After Function! 출력
  6. 메인 스레드에 js 코드가 실행되어 더이상 call stack에 실행할 스택이 없어 비워지게 된다.
  7. 그러면 event 핸들러가 이를 감지하여 Microtask Queue에 있는 async 함수를 빼와 Call stack에 적재하게 된다.
  8. Promise 객체의 결과물인 One! 출력

결과

> Before Function!
> In function!
> After Function!
> One!

그렇다면 myFunc() 함수를 호출하는 메인 스택에서 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();
console.log('After Function!');

내가 생각한 순서
1. Before Function! 출력
2. myFunc() 함수 전부 Microtask queue에 적재
3. After Function! 출력
4. 메인 스레드에 call stack이 전부 비워져 있음으로 call stack으로 옮겨지고 myFunc() 실행
5. In function! 출력
6. one으로 인하여 다시 Microtask queue에 적재
7. call stack에 아무것도 없으므로 One 출력

결과

> Before Function!
> In function!
> One!
> After Function!

여기서부터 어려웠다. Microtask Queue의 콜백들은 우선 메인 Call stack이 비워져 있어야 이벤트 루프가 옮겨 처리한다. After Function!이 있음에 불구하고 왜 myFunc()가 먼저 실행된걸까?
나는 여기서 소름이 돋았다.
나도 async/await을 사용하면서 맨날 이렇게 사용하고 있었는데; 생각을 안하고 산 것 같다.
그래서 맨날 비동기 동기가 헷갈렸던 것구나 정말 공부를 해야겠다.. ㅜ

다시 생각해보니 메인에서 아예 await 키워드를 사용했으니 메인 전체가 Microtask queue에 들어간건가? 생각했다.
그러면 아래 순서가 이렇게 될 것 같았다.

  1. Task queue에 console.log('Before Function!') 적재
  2. Microtask Queue에 await myFunc() 적재
  3. Task queue에 console.log('After Function!'); 적재

근데 이렇게 되면 In function!이 먼저 출력되어서 이것도 아니구.. 왜일까?

Async/Await의 진짜 동작

let x = await bar(); // bar() 함수 정의는 생략
console.log(x);
console.log('Done');

사실 위의 코드는 아래 코드와 같은 의미이다.

bar().then(x => { 
    console.log(x); 
    console.log('Done'); 
});

읭? 몬소리여 하고 다시 보니 await bar(); 이후 실행되는 동일 라인의 코드들은 모두 await bar()의 then 핸들러의 콜백 함수로 들어간다는 뜻이다.

실제로 async/await 키워드는 promise.then() 메소드를 사용하지 않고도 비동기 코드를 작성할 수 있게 해주는 문법적인 편의 기능일 뿐이다.

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 => {
	  console.log(res);
	});
}

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

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

profile
https://nibble2.tistory.com/ 둘 중에 어떤 플랫폼을 써야할지 아직도 고민중인 사람

0개의 댓글