JS Event Loop

kim yeeun·2024년 2월 21일
1

JS는 모두가 알다시피, 싱글 쓰레드 언어이며 위와 같이 한번에 하나의 코드를 실행할 수 있다는 것을 뜻한다.
이는 곧 한번에 하나의 콜 스택을 가질 수 밖에 없다는 것을 말한다.

function multiply(a,b) {
  return a*b;
}

function square(n) {
  return multiply(n,n);
}

function printSquare(n) {
  var squared = square(n);
  console.log(squared);
}

printSquare(4);

제곱해서 출력해주는 아주 기본적인 함수이다. 여기서 콜 스택을 보게 되면,

호출한 순서대로 쌓이게 됩니다.

이런 방식이 기본적인 콜 스택이며 웹 브라우저에서 에러가 발생했을 때 그때까지의 호출순서를 보여주는 것도 콜 스택을 통해서 하는 것이다. 만약 콜 스택을 재귀함수로 호출하게 되면 아래와 같은 경고문구가 나오면서 종료된다.

RangeError: Maximum call stack size exceeded!!

블로킹(Blocking)

var foo = $.getSync('//foo.com');
var bar= $.getSync('//bar.com');
var qux = $.getSync('//qux.com');

console.log(foo);
console.log(bar);
console.log(qux)

jQuery를 통해서 동기적으로 네트워크 요청을 3번 하는 코드이며 다음과 같이 콜 스택이 진행된다.

네트워크 요청은 느린 작업이기 때문에 다음 작업이 곧장 실행되지 않고 현재 진행되는 작업이 끝날 때까지 기다린 후에 다음 작업이 실행되고 있다. 이 방식이 문제가 되는 이유는 바로 코드가 웹 브라우저에서 실행되고 있기 때문이다.

느린 작업으로 인해 blocking이 발생하게 되면 웹 브라우저는 렌더링을 하지 못하고 다른 코드 또한 실행할 수 없게 된다. 즉, 사용자의 경험을 막게 된다. 따라서 다른 방식을 통해서 다음과 같은 작업을 해결해야 하는데 그걸 위한 것이 바로 “비동기 콜백” 이다.

비동기 콜백(Asynchronous Callback)

일반적으로 비동기 콜백을 설명할 때 가장 많이 사용하는 함수가 바로 setTimeout 이다.
주어진 시간만큼 기다렸다가 콜백함수를 실행하는 이 함수는 JS엔진인 V8에 내장되어 있지 않고,
웹 브라우저에서 제공하는 Web API에 존재한다.
아래 코드를 보면서 비동기 콜백이 어떻게 이루어지는지 확인하자.

console.log('First Stack');
setTimeout(function callback(){
  console.log('Asynchronous Callback');
}, 0);
console.log('Second Stack');

지금까지 배운 콜 스택의 개념을 활용하면 콜 스택에 차례대로 쌓일 것 같지만, V8의 소스코드에는 setTimeout 함수가 없기 때문에 웹 브라우저가 대신 실행해주어야 한다.

처음엔 순서대로 쌓이다가 setTimeout 함수를 웹 브라우저에게 맡기고 두 번째 log 를 쌓는다. 따라서 아래와 같이 먼저 출력되는 것이다.

First Stack
Second Stack
Asynchronous Callback

이제 콜 스택에서 main() 을 제외한 모든 함수가 리턴되고,
Web API의 setTimeout 타이머가 종료되면 해당 콜백이 콜백 큐로 전달된다.
이제 여기서 이벤트 루프의 역할이 나오는데 이벤트 루프는 콜 스택과 콜백 큐를 감시하는 역할로 콜백 큐에 함수가 존재하고 콜 스택이 비었다면 콜백 큐에서 콜백을 꺼내 콜 스택에 넣어주는 역할을 한다.

GIF

MicroTask Queue

setTimeout(function () {
  // (A)
  console.log('A');
}, 0);
Promise.resolve()
  .then(function () {
    // (B)
    console.log('B');
  })
  .then(function () {
    // (C)
    console.log('C');
  });

Async/Await

Async/Await 문법은 ES8부터 사용가능합니다.
async 키워드로 Promise를 반환하는 비동기 await 함수를 만들 수 있습니다.

Promise.resolve('Hello!');

// 위 코드는 아래 코드와 같다.

async function greet() {
  return 'Hello!';
}

비동기 함수가 Promise를 반환하는데 await 키워드를 비동기 함수 앞에 붙여주면 비동기 함수가 Promise를 반환할때 까지 코드를 일시 중지 할 수 있다.

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. In function!
// 3. After function!
// 4. One!

Before function!이 실행되었고, myFunc 함수 내부의 In function!이 먼저 찍혔다.

function a() {
  console.log('a1');
  b();
  console.log('a2');
}

function b() {
  console.log('b1');
  c();
  console.log('b2');
}

async function c() {
  console.log('c1');
  setTimeout(() => console.log('setTimeout'), 0);
  await d();
  console.log('c2');
}

function d() {
  return new Promise(resolve => {
    console.log('d1');
    resolve();
    console.log('d2');
  }).then(() => console.log('then!'));
}

a();

a 함수 호출, console.log 실행, 출력 → a1

b 함수 호출, console.log 실행, 출력 → b1

c 함수 호출, console.log 실행, 출력 → c1

setTimeout이 Task Queue에 쌓임.

d 함수 호출, 첫 번째 console.log 실행, 출력 → d1 (비동기X)

두 번째 console.log 실행, 출력 → d2 (비동기X)

.then 콜백은 백그라운드를 거쳐 마이크로 태스크 큐에 쌓임

d 함수 호출 완료 후 await를 만나고 async 함수 c는 중단

async 함수의 나머지는 마이크로 태스크 큐에 쌓임

c 함수를 호출한 실행 컨텍스트(b함수)로 돌아가서 console.log 실행, 출력 → b2

b 함수를 호출한 실행 컨텍스트(a함수)로 돌아가서 console.log 실행, 출력 → a2

Call Stack이 모두 비워지고, Event Loop가 MicroTask Queue를 확인. then 콜백, async 함수가 쌓여있음.

.then 콜백 실행, console.log 출력 → then!

async 함수 중단된 곳부터 이후로 실행, console.log 출력 → c2

또다시 Event Loop가 Task Queue를 확인, setTimeout의 콜백이 쌓여있음. setTimeout의 콜백을 Call Stack으로 옮겨 실행 및 출력 → setTimeout

a1
b1
c1
d1
d2
b2
a2
then!
c2
setTimeout
profile
안녕하세요 프론트엔드 엔지니어 김예은입니다.

0개의 댓글