9/19/2023

Asynchronous JavaScript

Synchronous vs. Asynchronous

비동기의 개념을 이해하기 전에 동기의 개념을 살펴본다. 동기란 코드가 한 줄씩 실행되는 것을 의미한다. 실행 컨텍스트의 부분인 실행 스레드는 컴퓨터의 CPU에서 코드를 실제로 실행한다. 각 줄의 코드는 항상 이전 줄의 실행이 끝날 때까지 기다리는데 만약 한 줄의 코드가 실행하는 데 오랜 시간이 걸릴 때 문제를 일으킬 수 있다. 오랜 시간이 걸리는 작업은 코드 실행을 차단하기 때문에 문제가 된다.

이와 반대로, 비동기 코드는 백그라운드에서 실행 중인 작업이 완료된 후에 실행된다. 따라서, 비동기 코드는 논-블록킹(나머지 코드는 정상적으로 계속 실행)이고 실행은 비동기 작업의 완료를 기다리지 않는다. 요약하면, 비동기 프로그래밍은 일정 시간 동안 프로그램의 동작을 조율하는 것으로 비동기는 말 그대로 같은 시간에 발생하지 않는 것을 의미한다. 비동기를 구현하기 위해 콜백 함수가 필요하지만 콜백 함수 자체가 코드를 자동으로 코드를 비동기로 만들지는 않는다. 배열의 map() 메서드는 콜백 함수를 받지만 코드를 비동기적으로 만들지 않는다. setTimeout과 같은 일부 함수들만이 비동기 방식으로 동작한다. 또 하나 중요한 점은 콜백 함수와 마찬가지로 이벤트 리스너 자체만으로 코드를 비동기적으로 만들지 않는다는 것이다. 예를 들어, 버튼 클릭을 대기하는 이벤트 리스너는 백그라운드에서 그냥 클릭을 기다리고 있을 뿐이며 아무 작업도 수행하지 않는다. 그래서 어떠한 비동기적 동작도 포함되어 있지 않다. 중요한 것은 타이머 실행 혹은 이미지 로드와 작업의 비동기적인 동작이다.


AJAX(Asynchronous JavaScript And XML)

AJAX 호출은 비동기 JavaScript의 가장 중요한 사용 예시인데 AJAX는 비동기 JavaScript와 XML의 약자로 원격 웹 서버와 비동기적으로 통신할 수 있도록 해준다. 코드에서 AJAX 호출을 수행하여 웹 서버에서 데이터를 동적으로 요청한다. 즉, 페이지를 로드하지 않고 애플리케이션에서 데이터를 동적으로 사용할 수 있다. XML은 데이터 형식이며 과거에는 웹에서 데이터를 전송하는 데 널리 사용되었다. 그러나 요즘은 더 이상 API에서 XML 데이터를 사용하지 않는다. AJAX라는 용어는 예전에 매우 인기 있었던 오래된 이름일 뿐이며 현재도 여전히 사용되지만 더 이상 XML을 사용하지 않는다. 그 대신 요즘 대부분의 API는 JSON 데이터 형식을 사용한다. JSON은 오늘날 가장 인기 있는 데이터 형식인데 본질적으로 문자열로 변환된 JavaScript 객체이기 때문이다. 그러므로 웹을 통해 보내고 데이터가 도착하면 JavaScript에서 사용하는 것이 매우 쉽다.

JavaScript에서 여러 방법으로 AJAX 호출을 수행할 수 있는데 가장 오래된 방법은 XML HTTP 요청 함수이다. 오래된 방법이지 이 함수를 인지하고 있어야 하는 이유는 필요한 경우 XML HTTP 요청이 존재한다는 사실과 프로미스 이전의 AJAX 호출이 이벤트와 콜백 함수로 처리되는 방식을 보여주기 때문이다. 다음의 코드에서 알 수 있듯이 콜백 함수가 너무 많이 중첩될 경우 콜백 지옥이라고 하는데 콜백 지옥이란 비동기 작업을 순차적으로 실행하기 위해 많은 중첩된 콜백 함수가 있는 경우로 AJAX 호출뿐만 아니라 콜백 함수로 처리되는 모든 비동기 작업에서 발생한다. 콜백 지옥은 쉽게 식별할 수 있는데 바로 삼각형 모양이다. 콜백 지옥의 문제는 코드를 매우 지저분하게 만드는데 더 심각한 문제는 코드를 유지하기 어렵게 만들고 이해하기 어렵게 만드는데 이러한 코드는 버그가 많으며 질이 떨어진다. ES6 이후로 프로미스라는 것을 사용하여 콜백 지옥을 벗어날 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const getCryptoInfo = function (coinname) {
  const crypto = new XMLHttpRequest();
  crypto.open(''GET''`https://api.coincap.io/v2/assets/${coinname}`);
  crypto.send();
 
  // 비동기적으로 실행되기 때문에 콜백 함수를 등록한다.
  // 데이터가 도착하면 load 이벤트를 발산한다.
  crypto.addEventListener(''load''function () {
    const { data: cryptoData } = JSON.parse(this.responseText); // 구조 할당.
    console.log(cryptoData);
 
    const id = cryptoData.id;
 
    const rates = new XMLHttpRequest();
 
    // rates AJAX 호출은 crypto AJAX 호출에 의존한다. 즉, 중첩 콜백 함수.
    // 콜백 지옥: 콜백 함수 안의 콜백 함수가 너무 많아지는 상황.
    rates.open(''GET''`https://api.coincap.io/v2/rates/${id}`);
    rates.send();
 
    rates.addEventListener(''load''function() {
      const { data: ratesData } = JSON.parse(this.responseText);
      console.log(ratesData);
    });
  });
};
 
getCryptoInfo("bitcoin")
cs


Promise

프로미스에 대해 알아보기 전 XML HTTP 요청 함수를 현대적인 AJAX 호출 방식으로 변경하는데 바로 fetch 함수로 이 함수는 프로미스를 즉시 반환한다.
1
2
3
4
5
6
7
8
9
const getCryptoInfo = function (coinname) {
  // json() 메서드는 fetch 함수에서 오는 모든 응답 객체에 사용 가능한 메서드
  // 문제는 이 json 함수 자체도 사실 비동기 함수, 새로운 프로미스를 반환한다.
  fetch(`https://api.coincap.io/v2/assets/${coinname}`)
    .then((res) => res.json())
    .then((crpyto) => console.log(crpyto));
};
 
getCryptoInfo("bitcoin");
cs

프로미스의 공식 정의는 비동기 작업의 미래 결과를 나타내는 데 사용되는 객체로 쉽게 말하면 프로미스는 비동기적으로 전달된 값을 담는 컨테이너이다. 위 코드의 AJAX 호출의 응답은 미 값의 완벽한 예시인데 AJAX 호출 시 값이 바로 전달되지는 않지만 미래에 특정 값이 응답으로 돌아오기 때문이다.

프로미스는 두 가지 큰 이점을 가지는데 하나는 비동기 결과를 처리하기 위해 비동기 함수로 전달된 이벤트와 콜백 함수에 의존할 필요가 없으며 이벤트와 콜백 함수는 예측할 수 없는 결과를 초래할 수 있다는 점을 고려하면 이는 큰 이점이다. 나머지 하나는 콜백 함수를 중첩하는 대신 비동기 작업의 연속을 위해 프로미스를 연쇄적으로 사용할 수 있으며 이를 통해 콜백 지옥에서 벗어날 수 있다.

프로미스는 비동기 작업과 함께 작동하기 때문에 시간에 따라 변한다. 즉, 프로미스는 다른 상태에 있을 수 있으며 이것을 프로미스의 주기라고 부른다. 처음에 프로미스는 보류 중(pending)이며 이는 비동기 작업에서 나오는 값이 사용 가능하지 않은 상태이다. 이 시간 동안 비동기 작업은 여전히 백그라운드에서 수행하고 있다. 그런 다음 작업이 마칠 때 프로미스가 해결(settled)되었다고 말하며 두 가지 종류의 해결된 프로미스가 있다. 이행된(fulfilled) 프로미스와 거부된(rejected) 프로미스이다. 이행된 프로미스는 성공적으로 값을 생성한 프로미스이지만 거부된 프로미스는 비동기 작업 중에 오류가 발생한 것을 의미한다. 프로미스에 대한 중요한 점은 프로미스는 한 번만 해결된다. 그래서 그 이후에는 상태가 영원히 변경되지 않는다. 즉, 프로미스는 이행되었거나 거부되었지만 그 상태를 변경하는 것은 불가능하다.

이행된 상태를 처리하려면 모든 프로미스에서 사용 가능한 then() 메서드를 사용한다. 그런 다음 then() 메서드에는 실제로 프로미스가 이행될 때 실행되길 원하는 콜백 함수를 전달해야 한다. 콜백 함수는 호출되면 JavaScript에 의해 하나의 인자를 받는데 그 인자는 이행된 프로미스의 결과 값이다. then() 메서드는 실제로 어떤 것을 반환하든 상관없이 항상 프로미스를 반환하는데 만약에 값을 반환하면 그 값은 반환 프로미스의 이행 값이 된다. 다음 코드에서 알 수 있듯이 콜백 지옥 대신에 플랫 체인(Flat chain) 프로미스를 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const getCryptoInfo = function (coinname) {
  // json() 메서드는 fetch 함수에서 오는 모든 응답 객체에 사용 가능한 메서드.
  // 문제는 이 json 함수 자체도 사실 비동기 함수, 새로운 프로미스를 반환한다.
  fetch(`https://api.coincap.io/v2/assets/${coinname}`)
    .then((res) => res.json())
    .then((crpyto) => {
      const { data } = crpyto;
      const id = data.id;
      return fetch(`https://api.coincap.io/v2/rates/${id}`);
    })
    .then(res => res.json())
    .then(rates => console.log(rates));
};
 
getCryptoInfo("bitcoin");
cs

만약 프로미스가 거부되면 처리하는 두 가지 방법이 있는데 첫 번째 방법은 then() 메서드에 두 번째 콜백 함수를 전달하는 것이다. 1번 콜백 함수는 항상 이행된 프로미스에 대해 호출되지만 프로미스가 거부된 경우 호출될 2번 콜백 함수를 전달할 수 있다. 하지만 이 방법은 모든 then() 메서드에 오류 처리를 추가해야 하기 때문에 코드가 중복되는 단점을 가진다. 모든 오류를 전역적으로 처리하는 더 나은 방법이 있는데 바로 체인 어디에서든 발생하는 오류를 체인 끝에서 catch () 메서드를 추가함으로써 처리할 수 있다. 동일한 콜백 함수를 사용할 수 있는데 여기의 콜백 함수는 발생한 오류 객체와 함께 호출되기 때문이다. 체인 끝에 있는 catch() 메서드는 체인 전체에서 발생하는 모든 오류를 캐치할 수 있는데 오류는 체인 아래로 전파되며 처리되기 전까지 계속되기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const getCryptoInfo = function (coinname) {
  fetch(`https://api.coincap.io/v2/assets/${coinname}`)
    .then((res) => res.json())
    .then((crpyto) => {
      const { data } = crpyto;
      const id = data.id;
      return fetch(`https://api.coincap.io/v2/rates/${id}`);
    })
    .then((res) => res.json())
    .then((rates) => {
      const { data } = rates;
      const rateUsd = data.rateUsd;
      console.log(rateUsd);
    })
    .catch((error) => console.log(error));
};
 
getCryptoInfo("bitcoin");
cs

finally() 메서드는 모든 프로미스에서 사용할 수 있는 메서드로 여기에 정의된 콜백 함수는 프로미스가 이행되었든 거부되었든 상관없이 항상 호출된다. 따라서, 프로미스의 결과에 관계없이 항상 발생해야 하는 작업에 사용한다. 만약 then() 메서드 중 하나에서 오류를 생성하고 던지면 프로미스가 즉시 거부된다. 즉, 여기에 의해 반환된 프로미스는 거부된 프로미스가 되며 catch() 메서드까지 전파된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const getCryptoInfo = function (coinname) {
  fetch(`https://api.coincap.io/v2/assets/${coinname}`)
    .then((res) => res.json())
    .then((crpyto) => {
      const { data } = crpyto;
 
      // 오류 처리.
      if (!data) {
        throw new Error(''존재하지 않는 코인'');
      }
 
      const id = data.id;
 
      return fetch(`https://api.coincap.io/v2/rates/${id}`);
    })
    .then((res) => res.json())
    .then((rates) => {
      const { data } = rates;
      const rateUsd = data.rateUsd;
      console.log(rateUsd);
    })
    .catch((error) => console.log(error))
    .finally(() => {
      console.log("2020-2021년 투자는 유동성!");
    });
};
 
getCryptoInfo("bitcoin");
cs

프로미스를 생성하려면 프로미스 생성자를 사용한다. 프로미스는 특수한 종류의 객체이다. 프로미스 생성자는 정확히 하나의 인수를 받는데 그것이 바로 실행자(executor) 함수이라고 불리는 것이다. 프로미스 생성자가 실행되는 즉시 실행자 함수를 자동으로 실행하는데 두 가지 다른 인자를 전달한다. 바로 resolve 함수와 reject 함수이다. 실행자 함수는 프로미스로 처리하려는 비동기 동작을 포함하는 함수이다. 생성자 함수는 최종적으로 결과 값을 생성하는데 이 값은 프로미스의 미래 값을 나타낸다. resolve 함수를 호출하면 프로미스를 이행된 상태로 표시한다. 이행된 값을 전달하여 then() 메서드에서 소비할 수 있다. 이와 반대로 reject 함수를 호출하면 프로미스를 거부된 상태로 표시하며 전달된 값은 catch() 메서드에서 사용할 수 있는 오류이다. 프로미스화(promisifying)는 콜백 기반의 비동기 동작을 프로미스 기반으로 변환하는 작업이다. 이를 통해 콜백 지옥을 해결할 수있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const sleepDecider = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() >= 0.5) {
      resolve("잠자러...");
    } else {
      reject(new Error("작업중..."));
    }
  }, 3000);
});
 
sleepDecider.then((res) => console.log(res)).catch((err) => console.error(err));
 
const cycle = (sec) => {
  return new Promise((resolve) => {
    setTimeout(resolve, sec * 1000);
  });
};
 
cycle(2)
  .then(() => {
    console.log("조금 만 더하고 자야겠다!");
    return cycle(2);
  })
  .then(() => console.log("잠이 너무 오네..."));
cs

Promise Combination Function

프로미스를 연속적으로 실행하는 대신 병렬로 실행할 수 있다. 즉, 동시에 모두 실행할 수 있다. Promise.all() 메서드는 정적 메서드로 프로미스 배열을 받고 배열의 모든 프로미스를 동시에 실행하는 새로운 프로미스를 반환한다. 중요한 점은 하나의 프로미스가 거부되면 전체가 거부된다. 다시 말해, 하나의 프로미스가 거부되면 Promise.all() 메서드는 바로 중단된다. 하나의 거부된 프로미스가 전체를 거부하는 데 충분하기 때문이다. 요약하면, Promise.all() 메서드는 여러 비동기 작업을 동시에 수행해야 하는 상황에서 서로 의존하지 않는 작업들이라면 항상 병렬로 실행해야 할 때 사용하는 메서드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const getJSON = async function (url) {
  const res = await fetch(url);
  const { data } = await res.json();
 
  return data;
};
 
const getFiveCryptos = async function (coinname1, coinname2, coinname3) {
  try {
    // 서로 의존성이 없는 비동기 작업들이 순차적으로 실행 -> 비효율적!
    // const coin1 = await getJSON(
    //   `https://api.coincap.io/v2/assets/${coinname1}`
    // );
    // const coin2 = await getJSON(
    //   `https://api.coincap.io/v2/assets/${coinname2}`
    // );
    // const coin3 = await getJSON(
    //   `https://api.coincap.io/v2/assets/${coinname3}`
    // );
 
    // console.log([coin1, coin2, coin3]);
 
    // 서로 관련없는 프로미스를 병렬로 실행한다.
    const coins = await Promise.all([
      getJSON(`https://api.coincap.io/v2/assets/${coinname1}`),
      getJSON(`https://api.coincap.io/v2/assets/${coinname2}`),
      getJSON(`https://api.coincap.io/v2/assets/${coinname3}`),
    ]);
 
    console.log(coins.map((coin) => coin.priceUsd));
  } catch (error) {
    console.error(error);
  } finally {
    console.log("1929...");
  }
};
 
getFiveCryptos("bitcoin""ethereum""solana");
cs

Promise.race() 메서드는 프로미스 배열을 받고 프로미스를 반환한다. Promise.race() 메서드가 반환하는 프로미스는 입력 프로미스 중 하나가 해결될 때 바로 해결된다. 해결되었다는 것은 값이 사용 가능하다는 것을 의미하며 거부되었는지 이행되었는지는 중요하지 않다. 즉, Promise.race() 메서드는 첫 번째로 해결된 프로미스를 반환한다. 그렇기 때문에 Promise.race() 메서드는 하나가 해결되면 바로 중단된다. 다시 말하지만, 이행 혹은 거부 여부는 상관없다. 이러한 성질 때문에 끝나지 않는 프로미스 혹은 아주 오랜 시간이 걸리는 프로미스를 방지할 때 매우 유용하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const getJSON = async function (url) {
  const res = await fetch(url);
  const { data } = await res.json();
 
  return data;
};
 
const getFirstCrypto = async function (coinname1, coinname2, coinname3) {
  // 가장 먼저 해결된 프로미스를 반환한다.
  // 해결은 이행 혹은 거부이다.
  const coin = await Promise.race([
    getJSON(`https://api.coincap.io/v2/assets/${coinname1}`),
    getJSON(`https://api.coincap.io/v2/assets/${coinname2}`),
    getJSON(`https://api.coincap.io/v2/assets/${coinname3}`),
  ]);
 
  console.log(coin);
};
 
getFirstCrypto("bitcoin""ethereum""solana");
 
// 끝나지 않거나 시간이 너무 오래 걸리는 프로미스를 방지한다.
const timeout = function (seconds) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error("It took too long..."));
    }, seconds * 1000);
  });
};
 
Promise.race([getJSON(`https://api.coincap.io/v2/assets/tether`), timeout(0.5)])
  .then((coin) => console.log(coin))
  .catch((error) => console.error(error));
cs

Promise.allSettled() 메서드는 ES2020에서 도입된 메서드로 프로미스 배열을 입력으로 받고 모든 해결된 프로미스의 배열을 반환한다. 다시 말해, 프로미스가 거부되었는지 여부에 상관없이 모든 해결된 프로미스를 반환한다. Promise.all() 메서드와 비슷하지만 Promise.all() 메서드는 하나의 프로미스가 거부되면 바로 중단되지만 Promise.allSettled는 절대로 중단되지 않으며 모든 프로미스의 결과를 반환한다. Promise.any() 메서드는 ES2021에서 도입되었으며 프로미스 배열을 입력으로 받고 첫 번째로 이행된 프로미스를 반환하며 거부된 프로미스는 무시한다. Promise.race() 메서드와 매우 유사하지만 거부된 프로미스를 무시한다는 차이가 있다. 따라서, Promise.any() 메서드의 결과는 모든 프로미스가 거부되지 않는 한 항상 이행된 프로미스이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.allSettled([
  Promise.resolve("1등"),
  Promise.resolve("2등"),
  Promise.reject("꽝!"),
])
  .then((res) => console.log(res))
  .catch((error) => console.log(error));
 
Promise.any([
  Promise.resolve("1등"),
  Promise.resolve("2등"),
  Promise.reject("꽝!"),
])
  .then((res) => console.log(res))
  .catch((error) => console.log(error));
cs

Async/Await

ES2017에 도입된 async/await 예약어는 프로미스를 더 쉽게 사용할 수 있도록 해준다. 먼저 async 예약어를 함수 이름 앞에 추가하면 async 함수가 된다. 즉, 함수는 이제 비동기 함수이며 코드를 실행하는 동안 백그라운드에서 계속 실행될 것이며 함수가 완료되면 자동으로 프로미스를 반환한다. 비동기 함수 내에서 await 문을 하나 이상 사용할 수 있다. 비동기 함수 내에서 await 예약어를 사용하여 프로미스의 결과를 기다릴 수 있다. 기본적으로 await 예약어는 함수 내에서 이 지점에서 실행을 중지하고 프로미스가 이행될 때까지 기다린다. 코드를 중지하면 실행이 차단된다라는 의문이 들 수 있는데 이 경우는 실제로 그렇지 않다. 왜냐하면 비동기 함수는 백그라운드에서 비동기적으로 실행되기 때문에 메인 실행 스레드를 블록하지 않는다. 따라서 호출 스택을 블록킹 하지 않는다. 이는 async/await가 특별한 이유인데 코드를 일반적인 동기 코드처럼 보이게 만들지만 내부적으로는 모든 것이 사실 비동기이기 때문이다. async/await는 사실상 프로미스의 then() 메서드에 대한 구문 설탕임을 이해해야 한다. 즉, 내부적으로는 여전히 프로미스를 사용하고 있으며 그저 다른 방식으로 프로미스를 소비하고 있는 것 뿐이다. async/await를 사용할 때 이전에 사용한 catch() 메서드를 사용할 수 없는데 catch() 메서드를 어디에든 첨부할 수 없기 때문이다. 대신, try-catch 블록을 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const getCryptoInfo = async function (coinname) {
  try {
    const assetResponse = await fetch(
      `https://api.coincap.io/v2/assets/${coinname}`
    );
    const assetData = await assetResponse.json();
 
    const { data: crpyto } = assetData;
 
    if (!crpyto) {
      throw new Error("존재하지 않는 코인");
    }
 
    const id = crpyto.id;
 
    const rateResponse = await fetch(`https://api.coincap.io/v2/rates/${id}`);
    const rateData = await rateResponse.json();
 
    const { data } = rateData;
    const rateUsd = data.rateUsd;
    console.log(rateUsd);
  } catch (error) {
    console.error(error);
  } finally {
    console.log("2020-2021년 투자는 유동성!");
  }
};
 
getCryptoInfo("bitcoin");
cs

비동기 함수는 항상 프로미스를 반환하는데 오류가 있더라도 이행된 프로미스를 반환하는 문제점이 있다. 만약 오류를 수정하고자 한다면 오류를 다시 던져야 한다. 즉, 오류를 다시 던져서 아래로 전파할 수 있도록 하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const getCryptoInfo = async function (coinname) {
  try {
    const assetResponse = await fetch(
      `https://api.coincap.io/v2/assets/${coinname}`
    );
    const assetData = await assetResponse.json();
 
    const { data: crpyto } = assetData;
 
    if (!crpyto) {
      throw new Error("존재하지 않는 코인");
    }
 
    const id = crpyto.id;
 
    const rateResponse = await fetch(`https://api.coincap.io/v2/rates/${id}`);
    const rateData = await rateResponse.json();
 
    const { data } = rateData;
    const rateUsd = data.rateUsd;
 
    return rateUsd;
  } catch (error) {
    // 오류를 다시 던져야 거부된 프로미스로 작동한다.
    throw error;
  }
};
 
// 프로미스와 async/await 혼합된 코드.
// getCryptoInfo("bitcoinaa")
//   .then((rate) => console.log(rate))
//   .catch((error) => console.error(error))
//   .finally(() => {
//     console.log("2020-2021년 투자는 유동성!");
//   });
 
// IIFE(즉시 호출 함수 표현식.)
(async function () {
  try {
    const rate = await getCryptoInfo("bitcoin");
    console.log(rate);
  } catch (error) {
    console.error(error);
  }
  console.log("2020-2021년 투자는 유동성!");
})();
cs


Event Loop

이벤트 루프는 JavaScript에서 비동기 동작을 가능하게 하는 핵심 요소로 JavaScript에서 논-블록킹 동시성 모델을 가질 수 있는 이유이다. 동시성(concurrency) 모델은 간단히 말하면 언어가 동시에 발생하는 여러 가지 작업을 어떻게 처리하는지를 나타낸다. JavaScript 엔진은 단일 스레드를 기반으로 구축되는데 엔진에는 실행 스레드가 하나뿐이라면 비동기 코드를 어떻게 논-블록킹 방식으로 실행할 수 있을까? 비동기 작업은 호출 스택에서 발생하지 않는데 이는 코드의 나머지 부분의 실행을 차단하기 때문이다. 이와 마찬가지로 콜백 함수도 비동기 작업이 발생하는 곳에 등록되는데 브라우저의 경우 웹 API 환경이다. 콜백 함수는 비동기 작업이 완료되기 전까지 그곳에 머무른다.

비동기 작업이 완료되면 콜백 함수는 콜백 큐로 이동한다. 이벤트 루프는 호출 스택을 살펴보고 비어 있는지 여부를 결정한다. 전역 컨텍스트를 제외하고 호출 스택이 비어 있다면 이는 현재 실행 중인 코드가 없다는 것을 의미한다. 이 경우 콜백 큐에서 첫 번째 콜백을 가져와서 호출 스택에 넣어 실행된다. 이를 이벤트 루프 틱이라고 한다. 이벤트 루프가 콜백 큐에서 콜백을 가져올 때마다 이벤트 루프 틱이 발생한다. 이벤트 루프는 호출 스택과 콜백 큐 사이의 조정을 하는 매우 중요한 작업을 수행한다. 즉, 이벤트 루프는 각 콜백 함수가 정확히 언제 실행되는지를 결정하는 주체이다. 이벤트 루프가 전체 JavaScript 런타임의 조율을 담당한다고 할 수 잇다. 이를 통해 JavaScript는 시간 개념을 가지고 있지 않다는 것인데 이는 비동기적으로 발생하는 모든 것이 JavaScript 엔진 내에서 일어나지 않기 때문이다. 모든 비동기 동작은 JavaScript 런타임에서 관리하며 어떤 코드가 다음에 실행될지를 결정하는 것은 이벤트 루프이다. JavaScript 엔진 자체는 단순히 주어진 코드를 실행할 뿐이다.

프로미스와 관련된 콜백 함수는 비동기 작업 완료 시 콜백 큐로 이동하지 않는다. 대신, 프로미스의 콜백 함수는 마이크로태스크(microtask) 큐라는 특수 큐로 이동한다. 마이크로태스크 큐에는 콜백 큐보다 높은 우선순위를 가진다. 이벤트 루프 틱이 끝날 때 이벤트 루프는 마이크로태스크 큐에 콜백이 있는지 확인하고 있으면 마이크로태스크 큐의 모든 콜백 함수를 실행한 후에 콜백 큐에서 추가 콜백 함수를 실행한다. 이는 마이크로태스크가 기본적으로 모든 다른 일반 콜백 함수들보다 먼저 실행될 수 있다는 것을 의미한다. 하나의 마이크로태스크가 추가되면 새로운 마이크로태스크는 콜백 큐의 어떤 콜백 함수보다 먼저 실행된다. 이것은 마이크로태스크 큐가 본질적으로 콜백 큐를 아사 시킬 수 있다는 것을 의미한다. 왜냐하면 마이크로태스크가 계속해서 추가되면 콜백 큐의 콜백들은 실행되지 못한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 출력 결과
// 시작
// 끝
// 이행1
// 이행2
// 타이머
console.log("시작");
 
// 0초가 지나도 바로 호출되지 않는 이유는 프로미스의 마이크로태스크 큐가 콜백 큐보다 더 높은 우선순위를 가진다.
setTimeout(() => console.log("타이머"), 0);
 
Promise.resolve("이행1").then((res) => console.log(res));
 
Promise.resolve("이행2").then((res) => {
  for (let i = 0; i < 1000000000; i++) {}
  console.log(res);
});
 
console.log("끝");
cs

update: 2023-10-04

댓글 없음:

댓글 쓰기