동기와 비동기 한방에 정리하기 (event loop, callback function, promise, async await)

냥무룩·2022년 2월 10일
0

Javascript

목록 보기
1/2

신입 면접 질문 단골로 나오는 동기와 비동기
알긴 아는데 말주변이 없어서 제대로 설명하지 못해 억울했기에 한번 쭉 정리해보기로 했다.
비전공자에게도 쉽게 이해할 수 있도록 모든 기초적인 것들을 한방에 다 모았으므로 양이 많을 수 있다.
하지만 동기 비동기의 전체적인 흐름을 이해하려면 조각조각 지식들을 한번에 모아서 정리할 필요가 있다고 생각했다.

우선 동기와 비동기의 정의에 대해 알아보자

동기와 비동기

동기(synchronous : 동시에 일어나는)

  • 동기는 말 그대로 동시에 일어난다는 뜻이다. 요청과 그 결과가 동시에 일어난다는 말인데 요청을 하면 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져야 하는 방식이다.

비동기(Asynchronous : 동시에 일어나지 않는)

  • 비동기는 반대로 동시에 일어나지 않는다는 뜻이며 요청과 결과가 동시에 일어나지 않는 방식이다. 자바스크립트 기준 대표적으로 setTimeout 과 Ajax 요청 등이 있다.

자바스크립트는 기본적으로 동기방식이다. (차례대로 동작)
정말 간단한 코드를 예로 들겠다.


이 결과는 누구나 알듯이


이 될것이다.

그렇다면 아래는 어떨까

위에서 부터 아래대로 실행이 된다면
1
(1초후에)2
3
이렇게 나와야 겠지만 결과는

1이 나오고 1초가 카운트되고 바로 3이 찍히고 1초가 다 되어서 2가 찍히게 된다.
그렇다 1 다음에 setTimeout이 시작되고 1초가 시작되는데 비동기로서 결과값을 받지 않고 그 다음 동기코드인 console.log(3)이 실행되고 1초가 다되어서 2가 찍히게 된 것이다.
자바스크립트는 이렇듯 비동기 작업과 동기작업으로 나뉘게 된다. 그 이유는 자원의 효율적인 분배 인데 이것을 쉽게 실생활의 예로 들어본다면
가령 우리가 카페에 갔다고 가정해보자
주문을 하는 작업을 세분화해보면

  1. 말을 하여 원하는 커피를 주문한다.
  2. 커피를 기다린다.
  3. 커피를 받는다.
  4. 자리로 돌아가 마신다.
  5. 카톡 메시지를 확인한다.
  6. 어제풀던 알고리즘 문제를 고민한다. 등등...

대략 이렇게 나뉠 것이다.
아까 setTimeout이 커피가 나오는 시간에 해당되고 커피를 받고 자리로 돌아가 마시는 것이 콜백함수에 해당할 것이다.
하지만 우리는 실생활에서 커피를 주문하고 나서 기다리는 시간동안 5번 이나 6번 작업을 이미 진행하고 있을 것이다. 그렇다! 시간의 효율적인 분배를 위해 우리는 이미 동기 비동기를 나누어 생활하고 있었다 :)

이것을 간단히 코드로 본다면


이런 형태가 될 것이다.
이렇듯 자바스크립트에서도 동기작업과 비동기 작업을 나누어 실행하게 된다

이것의 동작원리를 설명하자면 Event Loop에 대한 이해가 필요하다. 하지만 그 전에 콜백함수와 Promise, Async-Await에 대해 설명하고 가겠다.

콜백함수(callback function)

콜백함수란 파라미터로 함수를 전달 받아, 함수의 내부에서 실행하는 함수이다. 즉, 파라미터로 넘겨받은 함수는 일단 받고, 때가 되면 나중에 호출(callback) 한다는 것이 콜백함수의 기본 개념이다.

아까 위의 커피주문의 예에서 이미 보았듯이 콜백함수는 비동기동작의 진행후에 callback되어서 실행되는 함수를 말한다. 이는 커피를 받는다 마신다 와 같이 비동기 함수의 결과값을 처리하기 위한 방법으로 주로 사용된다.

그 전에 콜백함수에 대해 쫌 더 자세하게 보고 넘어가자.

이렇게 callback 함수가 실행 될 수 있는 것은 Javascript 함수가 1급 객체이기 때문이다.

1급 객체란

컴퓨터 프로그래밍에서 일반적으로 다른 객체들에 적용 가능한 연산을 모두 지원하는 객체를 의미한다.
즉, 다음과 같은 조건을 만족하면 1급객체이다.
1. 변수에 담을 수 있다.

let bar = function() {
	return 'JavaScript'
}
console.log(bar())
//JavaScript

bar라는 변수에 익명함수를 담았으며 잘 동작한다.

  1. 파라미터로 전달할 수 있다.
let number = [1, 2, 3];
number.forEach(function(x) {
	console.log(x*2);
})
//2
//4
//6

forEach를 예로 들어 익명함수를 파라미터로 전달 받았다.

  1. 반환 값으로 사용할 수 있다.
function test() {
  return function() {
    console.log('javascript');
  }
}

let bar = test()

bar();

// javascript

test() 함수의 반환값으로 콘솔에 출력하는 익명함수를 사용할 수 있다.

이렇듯 1급객체이기에 함수가 파라미터로 전달될 수 있고 이것이 콜백함수가 되는것이다.

콜백함수의 사용 원칙

  1. 익명 함수 사용
let number = [1, 2, 3];

number.forEach(function(x) {
  console.log(x * 2);
})

이렇게 파라미터로 익명함수를 사용할 수 있다.

  1. 함수의 이름만 넘기기
function team() {
	console.log('성웅 은규 정민 성민 세연 동건')
}

function work() {
	console.log('fungap 유지보수, 알고리즘 공부')
}

setTimeout(team, 2000)
setTimeout(work, 4000)

이미 작성되어있는 함수를 파라미터로 넘길 때는 이름만 넘기는 것이 원칙이다.

  1. 전역, 지역 변수를 콜백 함수의 파라미터로 전달 가능
let apple = 'apple' // Global Variable

function fruit(callback) {
  let banana = 'banana' // Local Variable
  callback(banana);
}

function eat(banana) {
  console.log(`apple: ${apple} / banana: ${banana}`);
}

fruit(eat)

// apple: apple / banana: banana

전역변수와 지역변수 모두 할당되어 사용이 가능해진다.

그렇다면 콜백함수가 우리 본래의 목표 비동기 값을 처리하는 예시를 살펴보자

콜백함수 비동기 처리 사용 방법의 예시들은 다음과 같다.

  1. 이벤트 리스너로 사용(버튼 클릭시 그 후의 동작 등등)
  2. 타이머 실행 함수로 사용(특정 시간 마다 실행하는 경우)
  3. 서버와 데이터를 주고받을 때 사용하는 Ajax와 같은 기능들에서 결과물을 받을 때

하지만 여기서 콜백지옥(Callback Hell) 이라는 문제가 발생하게 된다.

콜백지옥이란?

비동기 프로그래밍 시 발생하는 문제로서, 함수의 매개 변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들정도로 깊어지고 가독성이 매우 떨어지는 현상

이것을 예로 들어보겠다.

function square(x, callback) {
  setTimeout(callback, 1000, x * x);
}

square(2, function(x) {
  square(x, function(x2) {
    square(x2, function(x3) {
      console.log(x3)
    })
  })
})

// 256

현재 이 코드는 함수의 깊이가 3인데도 가독성이 매우 떨어지는 것을 알 수 있다. 가독성이 떨어지게 되면 유지보수도 힘들어지고 협업과정에서도 아주 비효율적이게 될 것이다.

이 상황을 해결하기 위해서 Promise와 Async/Await가 등장하게 된다.

Promise

콜백함수에서 설명했듯이 비동기함수 처리과정에서 콜백지옥의 가독성저하 문제를 해결하기 위해 등장한 것이 Promise이다.

Promise란

promise란 간단히 말하면 Javascript 비동기 처리에 사용되는 객체이다.

Promise 부분에서는 캡틴판교님의 Promise글이 너무 잘쓰여져 있어서 거의 완전히 비슷합니다. 아래의 링크를 참고해주세요
https://joshua1988.github.io/web-development/javascript/promise-for-beginners/

비동기 통신의 간단한 예를 들어서 설명해 보겠다.
먼저 간단한 ajax 통신 코드이다.

function getData(callbackFunc) {
 $.get('url 주소/products/1', function(response) {
   callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
 });
}

getData(function(tableData) {
 console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});

위 코드는 콜백함수를 이용해 제이쿼리의 ajax 통신 api를 통한 지정된 url에서 1번 상품 데이터를 받아오는 코드이다.

이 코드에 프로미스를 적용하면

function getData(callback) {
  // new Promise() 추가
  return new Promise(function(resolve, reject) {
    $.get('url 주소/products/1', function(response) {
      // 데이터를 받으면 resolve() 호출
      resolve(response);
    });
  });
}

// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
  // resolve()의 결과 값이 여기로 전달됨
  console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});

이런 코드가 된다.
아직 Promise를 모르기에 무슨 말인가 싶을 것이다.
걱정하지 마시라 아래에서 자세히 살펴보겠다.

Promise의 3가지 상태(states)

프로미스를 사용할 때 알아야 하는 가장 기본적인 개념이 바로 프로미스의 상태(states)이다. 여기서 말하는 상태란 프로미스의 처리 과정을 의미한다.
new Promise()로 프로미스를 생성하고 종료될 때 까지 3가지의 상태를 갖는다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태

  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태

  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

Pending(대기)

먼저 아래와 같이 new Promise() 메서드를 호출하면 대기(Pending) 상태가 된다.

new Promise()

new Promise() 메서드를 호출할 때 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject 이다.

new Promise(function(resolve, reject) {
  // 로직...

Fulfilled(이행)

콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행(Fulfilled) 상태가 된다.

new Promise(function(resolve, reject) {
  resolve();
});

그리고 이행 상태가 되면 아래와 같이 then()을 이용하여 처리 결과 값을 받을 수 있다.

function getData() {
  return new Promise(function(resolve, reject) {
    var data = 100;
    resolve(data);
  });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function(resolvedData) {
  console.log(resolvedData); // 100
});

Rejected(실패)

new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있다고 했는데 여기서 reject를 호출하면 실패(Rejected) 상태가 된다.

new Promise(function(resolve, reject) {
  reject();
});

그리고 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있다.

function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
  console.log(err); // Error: Request is failed
});

처리 흐름을 그림으로 나타내면 다음과 같다.

이제 위에서의 예제를 통해 설명해 보겠다.

function getData() {
  return new Promise(function(resolve, reject) {
    $.get('url 주소/products/1', function(response) {
      if (response) {
        resolve(response);
      }
      reject(new Error("Request is failed"));
    });
  });
}

// 위 $.get() 호출 결과에 따라 'response' 또는 'Error' 출력
getData().then(function(data) {
  console.log(data); // response 값 출력
}).catch(function(err) {
  console.error(err); // Error 출력
});

서버에서 제대로 응답을 받아오게 되면 resolve() 메서드를 호출하고, 응답이 없다면 reject() 메서드를 호출하게 된다. 호출된 메서드에 따라 then()이나 catch()로 분기하여 응답 결과 또는 오류를 출력한다.

여러 개의 프로미스 연결하기 (Promise Chaining)

콜백함수의 연결처럼 프로미스도 여러개의 프로미스를 연결하여 사용할 수 있다. 이번엔 setTimeout을 사용한 예제를 들어보겠다.

new Promise(function(resolve, reject){
  setTimeout(function() {
    resolve(1);
  }, 2000);
})
.then(function(result) {
  console.log(result); // 1
  return result + 10;
})
.then(function(result) {
  console.log(result); // 11
  return result + 20;
})
.then(function(result) {
  console.log(result); // 31
});

위 코드는 프로미스 객체를 하나 생성하고 setTimeout() 을 이용해 2초 후에 resolve()를 호출하는 예시이다.

resolve()가 호출되면 프로미스가 대기 상태에서 이행상태로 넘거가기 때문에 첫 번째 .then()의 로직으로 넘어가게된다. 첫 번째 .then()에서는 이행된 결과 값 1을 받아서 10을 더한후 그 다음 .then()으로 넘겨준다. 두 번째 .then()에서도 마찬가지로 바로 이전 프로미스의 결과 값 11을 받아서 20을 더하고 다음 .then()으로 넘겨준다. 마지막 .then()에서 최종 결과 값 31을 출력하게 된다.

그렇다면 앞에서 배운 콜백함수와 비교해 보자
우선 콜백함수의 코드이다.

const addFive = (number, callback) => {
  setTimeout(() => {
    callback(number + 5), 1000
  })
}
const addSix = (number, callback) => {
  setTimeout(() => {
    callback(number + 6), 1000
  })
}
const addSeven = (number, callback) => {
  setTimeout(() => {
    callback(number + 7), 1000
  })
}

const log = (number) => {
  console.log(number)
}

addFive(0, (number) => {
  addSix(number, (number) => {
    addSeven(number, log)
  })
})

복잡해 보이지만 위 코드를 설명하면 1초간격으로 addFive -> addSix -> addSeven이 실행되고 console.log로 출력하는 코드이다.

이것을 Promise로 표현하면

const addFive = (number) =>
	new Promise((resolve) =>
      setTimeout(() => {
        resolve(number + 5)
    }, 1000)
    )

const addSix = (number) =>
	new Promise((resolve) =>
      setTimeout(() => {
        resolve(number + 6)
    }, 1000)
    )

const addSeven = (number) =>
	new Promise((resolve) =>
      setTimeout(() => {
        resolve(number + 7)
    }, 1000)
    )

const log = (number) => 
new Promise((resolve) => setTimeout(() => console.log(number), 1000))

addFive(0)
  .then((number) => addSix(number))
  .then((number) => addSeven(number))
  .then((number) => log(number))

이렇게 된다.
코드 길이는 길어졌지만
then절을 붙여서 사용함으로서 가독성이 올라간 것을 볼 수 있다.

지금까지의 예제들은 너무 억지로 만들어진 예라는 느낌이 있었다. 실무에 있을 법한 예를 들어보겠다.

실무에서 있을 법한 프로미스 연결 사례

실제 웹 서비스에서 있을 법한 사용자 로그인 인증 로직에 프로미스를 여러 개 연결해 보겠다.

getData(userInfo)
  .then(parseValue)
  .then(auth)
  .then(diaplay);

위 코드는 페이지에 입력된 사용자 정보를 받아와 파싱, 진증 등의 작업을 거치는 코드를 나타내었다. 여기서 userInfo는 사용자 정보가 담긴 객체를 의미하고, parseValue, auth, display는 각각 프로미스를 반환해주는 함수라고 가정했다.

var userInfo = {
  id: 'test@abc.com',
  pw: '****'
};

function parseValue() {
  return new Promise({
    // ...
  });
}
function auth() {
  return new Promise({
    // ...
  });
}
function display() {
  return new Promise({
    // ...
  });
}

이렇게 Promise에 대해 알아보았는데 가독성을 조금 더 올렸을 뿐이지만 결국 Promise도 콜백함수를 사용하는 객체이므로 가독성이 많이 좋다고는 볼 수 없다.
그래서 나온것이 바로 Async/Await이다.

Async/Await

async와 await는 자바스크립트의 비동기 처리 패턴 중 가장 최근 나온 문법으로 기존 비동기 처리방식인 콜백 함수와 프로미스의 단점을 보완하고 가독성을 더 올리게 도와준다.

프로미스와 async 비교

프로미스를 보완했다고 하였는데 일단 비교를 통해 알아보겠다.
먼저 프로미스 코드다

function fruit() {
  return new Promise((resolve, reject) => {
    resolve("banana");
  })
}

const pocket = fruit()
console.log(pocket)
// Promise {<fulfilled>: "banana"}

그 다음은 async를 사용한 코드이다.

async function banana() {
  return "banana"
}
const pocket = banana()
console.log(pocket)
// Promise {<fulfilled>: "banana"}

위의 코드와 결과값이 같은 것을 볼 수 있다.

그렇다. 함수 앞에 async를 붙이기만 하면 리턴값이 promise 객체로 보내지게 된다.

우리가 new Promise로 만들고 resolve 쓰고 reject 쓰고 이런 귀찮은 일을 async 하나로 간단히 할 수 있다는 것이다!

그럼 await은?

await의 뜻은 기다리다. 즉, await은 비동기 처리를 동기적인 것 처럼 처리할 수 있게 해주는 코드이다!
await 또한 promise를 반환한다.

아래의 예제로 설명하겠다.

const delay = (sec) => {
  return new Promise((resolve) => setTimeout(resolve, sec))
}

async function apple() {
  await delay(2000)
  return "apple"
}

async function banana() {
  await delay(1000)
  return "banana"
}

async function pickFruits() {
  const getapple = await apple()
  const getbanana = await banana()
  return `${getapple} + ${getbanana}`
}

pickFruits().then(console.log)
// apple + banana

pickFruits가 호출되면 delay에 의해서 apple함수는 2초 banana함수는 1초 후에 값을 반환하게 되는데
await이 없다면 banana가 먼저 반환되고 apple이 그 다음에 반환되게 될 것이다. 하지만 await을 사용하여 동기처럼 값을 반환하기까지 기다리게 했으므로 순차적으로 값이 반환되게 된다!

이 예는 와닿지 않을 것 같아 실제로 쓸법한 예를 들어 설명해 보겠다.

function fetchUser() {
  let url = 'https://jsonplaceholder.typicode.com/users/1'
  return fetch(url).then(function(response) {
    return response.json();
  });
}

function fetchTodo() {
  let url = 'https://jsonplaceholder.typicode.com/todos/1';
  return fetch(url).then(function(response) {
    return response.json();
  });
}

위 함수들을 실행하면 각각 사용자 정보와 할 일 정보가 담긴 프로미스 객체가 반환된다고 가정하겠다.

두 함수를 이용하여 실행하게 될 로직은 다음과 같다
1. fetchUser()를 이용하여 사용자 정보 호출
2. 받아온 사용자 아이디가 1이면 할 일 정보 호출
3. 받아온 할 일 정보의 제목을 콘솔에 출력

async function logTodoTitle() {
  var user = await fetchUser();
  if (user.id === 1) {
    var todo = await fetchTodo();
    console.log(todo.title); // delectus aut autem
  }
}

이처럼 비동기 처리 코드를 동기적인 사고형태로 쉽게 표현할 수 있으므로 가독성이 매우매우 올라간다!!
콜백함수나 Promise를 사용했다면 코드길이도 엄청 길어지고 비교적 가독성이 떨어졌을 것이다.

지금까지 비동기처리에 관한 꽤나 긴 여정이었다.
그렇다면 이런 동기/비동기가 어떻게 브라우저나 node 엔진에서 실행되는지 동작원리를 알아보도록 하자!

Event Loop

앞서) 해당 글은 우리밋님의 유튜브 영상을 토대로 만들었습니다. 정말 좋은 영상이니 꼭 확인 바랍니다.
https://youtu.be/QFHyPInNhbo
https://www.youtube.com/watch?v=S1bVARd2OSE

위 그림은 Javascript 엔진의 구조이다.
간단히 용어들에 대해 설명하자면

Memory Heap
메모리 할당이 일어나는 곳

Heap
구조화되지 않은 넓은 메모리 영역 -> 객체(변수, 함수 등)들이 담긴다.

Call Stack (호출 스택)
실행될 코드의 한 줄 단위로 할당된다.

Web APIs (노드에서는 백그라운드로 설명된다)
비동기 처리를 담당한다.

Callback Queue (Task Queue, Event Queue 등 다양한 형태로 설명된다)
비동기 처리가 끝난 후 실행되어야 할 콜백 함수가 차례로 할당된다.

Event Loop
Queue에 할당된 함수를 순서에 맞춰 Call Stack에 할당해준다.

우선 동기적인 코드의 동작을 보겠다.

왼쪽의 코드는 실행되면 오른쪽과 같이 call stack에 담기게 된다. 그러면 위에서 부터 순차적으로 진행이 되어 콘솔을 출력하고 second() 함수를 만나 callstack에 console.log("두 번째")가

위 그림과 같이 쌓이게되고
콘솔 출력후 같은 방식으로 first()에 들어가 진행될 것이고 결과는

세번째 두번째 첫번째 세번째 이렇게 출력이 될 것이다.

그렇다면 이제 본격적으로 비동기적인 코드를 살펴보겠다.

console.log("시작")

setTimeout(function() {
  console.log("중간")
}, 3000)

console.log("끝")

위 의 코드 결과가

이렇게 나온다는 것은 이제 너무 잘 알 것이다.

그렇다면 javascript 동작 원리를 살펴보자
비동기적 코드는 callstack 만으로 해결할 수 없다.
아래의 그림을 보자

처음에 이렇게 시작이라는 콘솔이 callstack에 담기게 되고 그후에 비동기적 코드인 setTimeout을 만나게 된다. 그러면

위 그림과 같이 console에는 "시작" callstack에는 setTimeout이 담기게 되는데 이는 비동기작업 이므로 브라우저에서는 Web API 혹은 node에선 background로 넘어가게된다.


이렇게 되면 현재 web api에서는 3초가 흐르고 있을것이고 그다음 콜스택에는 console.log("끝")이 실행되어 console에는

그림과 같이 "끝"이 출력되고
setTimeout의 3초가 다 지나게되면 그 안의 익명함수가 Callback Queue로 넘어가게 된다.

이렇게 setTimeout은 사라지고 Callback Queue에 담긴 익명함수는 이벤트루프(돌아가는 화살표로 표현하였다)에 의해

이 그림과 같이 call stack이 비어있는 것을 확인하게 되면
Call Stack으로 다시 옮겨져 해당 함수가 실행되어 console.log("중간")이 call stack에 담기게된다.

그 후엔

이렇게 우리가 예상했던 결과인
시작

중간
이 출력되는 것이다!
신기하지 않은가! 이제서야 자바스크립트 동작에 대해 조금은 알것 같아졌다.

그렇다면, 앞에 배웠던 Promise도 포함해서 조금 더 자세히 알아보자.

이번엔 아까의 예에서 Promise 하나만 추가해 보겠다.

console.log("시작")

setTimeout(function () {
  console.log("중간")
}, 0)

Promise.resolve().then(function() {
  console.log("프로미스")
})

console.log("끝")

이것 또한 아까와 마찬가지로 console.log("시작")이 먼저 출력될것이고 setTimeout이 Web API로 들어가게 될것이다. 그림으로 표현하면

이렇게 될 것이다.
아마 여기까진 잘 따라올 것으로 믿는다.
그 후에는

Promise가 Call stack으로 들어오게 되고 .then절이 Web API로 가게될 것이다.

이렇게 될 것이다. 이번 예시에서는 setTimeout이 Promise보다 먼저 끝났다고 가정하고 진행하겠다.
그러면 Web API에 있던 비동기 작업이 끝나 그 콜백함수들이 Callback Queue에 순서대로 담길 것이다. 그것을 그림으로 나타내면

이렇게 될 것이다.
지금 상황에선 Event Loop는 Call Stack가 비어있는지 확인하고 비어있지 않기 때문에 작동하지 않는다.
그러면 이제 콘솔에 "끝"이 출력되고

이렇게 Call Stack이 비게 된다 그러면 Event Loop에 의해 Callback Queue의 작업들이 Call Stack으로 옮겨지게 될 건데 여기가 약간 중요하다.
Queue 이므로 선입선출이라 setTimeout소속 콜백함수가 더 먼저 call stack으로 갈 것 같지만 그렇지 않다!
바로 우선순위가 있기 때문이다.
그 우선순위는 잠시 후에 다루도록 하겠다.

우선순위에 의해 then의 콜백함수가 먼저 Call Stack에 담겨

이렇게 되고
콘솔에 프로미스가 출력이 된 후에

이렇게 setTimeout 익명함수가 담기게 되고

결과는

이렇게
시작

프로미스
중간
이 되게 된다!

그렇다면 아까 말한 우선순위에 대해 알아보자.

사실상 우리가 보았던 Callback Queue는 조금 더 복잡하게 되어있다.

이 그림이 되겠다.

Callback Queue 안에는 사실 Task Queue, Microtask Queue, AnimationFrames가 있다.

Task Queue에는 setTimeout이 담기게 되고, Microtask Queue에는 프로미스, Animation Frames에는 requestAnimationFrame이 담기게 된다

여기서 큐의 우선순위가 있게 되는데 항상 Microtask Queue > Animation Frames > Task Queue 순으로 진행되게 된다.

그러므로 후순위에 있는 Task Queue의 setTimeout의 함수는 Promise의 함수보다 더 늦게 실행된 것이다!

이렇게 이벤트 루프에 대해서 알아보았다.

적다보니 너무 글이 길어져 난잡해 졌을지 몰라도 하나하나의 지식들이 연관이 되어있기에 한번에 정리해 흐름대로 쭉 따라올 수 있게 하고 싶었다.

긴 글 읽어주셔서 감사합니다.

0개의 댓글