05.JavaScript-비동기

이수현·2022년 4월 30일
1

TIL

목록 보기
5/23

📚JavaScript 비동기

Javascript Runtime Environment

기본적으로 JavaScript는 동기적으로 실행된다.
-> 동기적이라는 것은 말 그대로 순차적으로 하나씩 진행된다는 것이다. 첫 번째 사진을 보면 알 수 있듯이 함수c()를 호출하면 b()를 호출하고 함수b()에서는 a()를 호출한다. 여기서 함수a()가 끝나기 전 까지는 함수b, c는 끝나지 못하고 기다리고 있다. 위 사진의 코드를 다시 작성해서 자세히 살펴보자.


function a() {
  for (let i = 0; i < 10000000000; i++);
  return 1;
}

function b() {
  return a() + 1;
}

function c() {
  return b() + 1;
}

console.log("Start!");
const result = c();
console.log(result);
// 위 코드는 처음에 Start!를 출력하고 함수c()를 호출하고 b()를 호출하고 a()를 호출하는데,
// 함수a()에서 for문을 줘서 끝나지 못하고 기다린다는게 무엇인지 확인할 수 있었다.
// for문이 다 끝나기 전까지 위 코드는 다음 코드를 실행하지 못하고 있다.
// for문이 다 끝나고 나서야 1을 리턴하고, 2를 리턴하고 마지막으로 3을 리턴해서
// console.log()를 통해 결과값이 찍히는 것을 확인할 수 있다.
// 이처럼 동기적이라는 것은 순차적으로 하나의 함수가 끝날 때 까지 다음 코드는 실행되지 않는다는 것을 의미한다.

반면에 2번째 사진을 보면, Callback QueueWeb APIEvent Loop를 확인할 수 있다.
3가지가 뭔지 알아보자.

  • Event Loop: Event Loop는 Call Stack과 Callback Queue를 계속해서 감시한다. 만약 Call Stack이 비어있다면, Callback Queue에서 가장 먼저 들어온 것을 Call Stack에 담아 실행시킨다.

  • Web API: ajax 요청, setTimeout(), 이벤트 핸들러의 등록과 같이 웹 브라우저에서 제공하는 기능들을 말한다. 그런데 여기서 중요한 것은 이러한 요청들의 처리가 JavaScript 엔진의 쓰레드와는 다른 쓰레드들에서 이뤄진다는 점이다.
    JavaScript 엔진의 Call Stack에서 실행된 비동기 함수가 요청하는 비동기 작업에 대한 정보와 콜백 함수를 Web API를 통해 브라우저에게 넘기면, 브라우저는 이러한 요청들을 별도의 쓰레드에게 위임하게 되는 것이다. 그 쓰레드는 해당 요청이 완료되는 순간 전달받았던 콜백 함수를 Callback Queue에 집어 넣는다.

  • Callback Queue: Callback Queue는 시간이 되었을 때 처리될 콜백함수들의 목록이다.
function execute() {
  console.log("1");
  setTimeout(() => {
    console.log("2");
  }, 3000);
  console.log("3");
}

execute();
// 예를 들어 위와 같은 execute() 함수가 있고, 이 함수를 호출하면
// 먼저 콘솔에 1을 출력하고,
// 다음에 setTimeout이라는 Web API를 마주치고, 이 안에 콜백 함수로 ()=>{console.log("2");} 를 전달하고,
// 지연 시간은 3000ms로 지정하고 해당 콜백 함수는 Callback queue에 담긴다.
// 다음 콘솔에 3이 찍힌다.
// Call Stack이 비어있으므로 Callback Queue에 있는 ()=>{console.log("2");}가
// Call Stack으로 담기고, 실행한다.
// 콘솔에 2가 출력되며 함수가 종료된다.
// 이처럼 1 2 3이 아닌 비동기적으로 1 3 2가 출력되는 것을 확인할 수 있다.
function a() {
  setTimeout(() => {
    console.log("a-setTimeout 콜백함수 호출");
  });
  console.log("a호출");
}

function b() {
  a();
  console.log("a() 호출 후 b() 콘솔 출력");
}

function c() {
  b();
  console.log("b() 호출 후 c() 콘솔 출력");
}

c();

// 함수로 다시 표현하면, c()를 호출하면 먼저 Call Stack:[c(),]
// c() 내부에서 b를 호출하면서 Call Stack:[c(), b(),]
// b() 내부에서 a를 호출하면서 Call Stack:[c(), b(), a()] 처럼 쌓인다.
// 그런 후에 a() 함수 안에 있는 setTimeout의 콜백 함수는 
// Callback Queue:[() =>{console.log("a-setTimeout 콜백함수 호출");}] 처럼 쌓인다.
// 그 후에 a() 함수 내부에 콘솔 a호출을 출력하고, Call Stack:[c(), b(),]
// a() 호출 후 b() 콘솔 출력을 출력하고 Call Stack:[c(),]
// b() 호출 후 c() 콘솔 출력을 출력하고 Call Stack: [] 처럼 비워진 시점에
// Callback Queue: Callback Queue:[] 비워지고,
// Call Stack: [() =>{console.log("a-setTimeout 콜백함수 호출");}] 이 담기고,
// a-setTimeout 콜백함수 호출을 출력하고 코드가 종료된다.

Callback Function

콜백함수란 다른 함수에 인수로 전달된 함수이며 외부 함수의 내부에서 호출돼 어떠한 루틴이나 작업을 완료한다. 이러한 콜백은 비동기 작업이 완료된 후 코드 실행을 계속하기 위해 자주 사용된다. 이를 비동기 콜백함수라고 한다.

Promise

Promise는 비동기 작업에 대한 완료 또는 실패한 이벤트와 결과 값을 표현한다.
Promise는 Promise가 생성될 때 전혀 알려지지 않은 값에 대한 proxy이다.
-> 이를 통해 핸들러를 비동기 작업의 최종 성공 값 또는 실패 이유와 연결할 수 있다.
-> 비동기 메서드가 동기 메서드와 같은 값을 반환할 수 있다.
-> 최종 값을 즉시 반환하는 대신 비동기 메서드는 미래의 특정 시점에 값을 제공하겠다는 Promise를 반환한다.

Promise의 상태

  • pending: 초기 상태. fulfilled되지도 rejected되지도 않음.
  • fulfilled: 작업이 성공적으로 완료됐음을 의미함.
  • rejected: 작업이 실패했음을 의미함.

pending상태인 Promise는 fulfilled or rejected 상태가 될 수 있다.
=> 이러한 상태들 중 하나가 발생하면 Promise의 then() 메서드에 의해 대기 중인 관련 핸들러가 호출된다.(여기서 핸들러는 이벤트가 발생할 때 전달되는 콜백함수?와 같은 것을 의미하는 것으로 보인다.)
=> 해당 핸들러가 연결될 때 Promise가 이미 fulfilled되거나 rejected된 경우 핸들러가 호출되므로 비동기 작업 완료와 연결되는 핸들러 사이에는 경쟁 조건이 없다.
=> Promise.prototype.then() 과 Promise.prototype.catch()와 같은 Promise 의 인스턴스 메서드는 Promise들을 반환하는데, 이것들은 연결지어질 수 있다.
출처 : MDN-Promise

위 사진을 보면 pending상태인 Promise가 fulfill되거나 reject될 수 있고, fulfill되거나 reject된 Prmoise는 그 대로 비동기 작업이나 에러 핸들링을 할 수도 있고,
다시 return된 Promisefmf 또 다시 [then(), catch()]로 연결지을 수 있다.
Promise를 몇 번이고 반환하는 작업을 한다면 그에 맞에 메서드들을 연결지어서 사용할 수 있다.

Chained Promises

  • Promise.prototype.then(onFulfilled, onRejected): 첫 번째 파라미터 onFulfilled는 콜백함수이며, Promise가 fulfilled되면 호출되어진다. 반대로 두 번째 파라미터 onRejected는 rejected되면 호출되어진다.
p.then(value => {
  // fulfillment
}, reason => {
  // rejection
});
// 위 코드를 보면 p는 Promise 객체이며, pending 상태인 Promise를 reject or fulfill 상태로 분기하려고 한다.
// 첫 번째 파라미터는 fulfilled되면 호출되어지는데,
// 호출되고나서 반환되는 값이 파라미터 함수의 인자인 value로 담겨 해당 값을 다룰 수 있다.
// 그리고 두 번째 파라미터의 reason에는 rejected된 이유인 결과가 reason에 담겨
// 해당 값을 다룰 수 있다.


- Promise.prototype.catch(onRejected): catch() 메서드는 Promise를 반환한다.
그리고 catch() 메서드는 rejected된 경우만 처리한다.
Promise.prototype.catch(onRejected) 메서드는 내부적으로 Promise.prototype.then(undefined, onRejected)를 호출하는 것과 동일하게 작동한다.(실제로 obj.catch(onrejected)를 호출하면 내부적으로 obj.then(undefined,onRejected)를 호출한다.....)


- Promise.prototype.finally(onFinally): finally() 메서드는 Promise를 반환한다.
Promise가 최종적으로 fulfilled되거나 rejected되었을 때 onFinally 콜백 함수가 실행된다.
이것은 Promise가 rejected, fulfilled 여부와 상관없이 코드를 실행하는 방법을 제공한다.
=> 그리고 이것은 Promise의 then(), catch() 메서드의 핸들러에서 코드가 중복되는 것을 방지하는 데 도움이 된다.

function checkMail() {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.5) {
      resolve('Mail has arrived');
    } else {
      reject(new Error('Failed to arrive'));
    }
  });
}

checkMail()
  .then((mail) => {
    console.log(mail);
  })
  .catch((err) => {
    console.error(err);
  })
  .finally(() => {
    console.log('Experiment completed');
  });
// 코드를 살펴보면 checkMail() 함수는 Promise를 반환하는데,
// 랜덤 숫자가 0.5보다 크다면 resolve(Message)처럼 resolve메서드에 Message를 반환하는 것을 볼 수 있다.
// 0.5보다 작은 경우에는 Error객체를 만들어서 reject함수에 넣어 호출하는 것을 볼 수 있다.
// 이러한 과정이 함수 선언문 밑에 호출 구문을 통해 알 수 있다.
// then() 안에 성공 메시지를 출력하는 콜백 함수를, 
// catch() 안에서는 실패 메시지를 출력하는 콜백 함수를 작성한 것을 확인할 수 있다.
// finally에는 위에 연결되어있는 then catch여부와 상관없이 무조건 실행되는 콜백 함수가 존재한다.
// 이 코드를 실행하면 첫 줄에는 Mail has arrived와 Failed to arrive이 랜덤으로
// 출력되지만 마지막에는 항상 Experiment completed이 출력된다.

Promise constructor

  • Promise(_) : 새로운 Promise 객체를 생성한다. 생성자는 주로 Promise를 지원하지않는 함수들을 래핑하는 데 사용된다. new Promise((resolve, reject) => {});

Promise Static Method

  • Promise.all(iterable): all()의 파라미터의 타입은 iterable 객체이다. 예를 들면, 비동기 작업들이 배열로 들어갈 수 있다.
    왜 all()이라는 메서드를 만들어서 비동기 작업들을 굳이 배열을 전달해가며 사용을 할까?..
    then().then() 체이닝하면서 사용해도 될텐데 만든 이유를 찾아보자.
    -> 병렬적으로 처리해서 기존의 체이닝 방식보다 시간이 된다.
  • Promise.allSettled(iterable): all()과는 다르게 error가 발생하는 Promise를 반환하는 작업들이 있어도 에러 메시지와, 성공한 Promise들의 반환값을 출력한다.
  • Promise.any(iterable): Promise중 하나가 이행 되자마자, 해결되는 단일 Promise을 이행된 Promise의 값과 함께 반환한다.
    -> 첫번째 Promise를 반환하는데 유리하다.
  • Promise.race(iterable): 주어진 Promise중에 제일 빨리 수행된 것이 반환된다.
  • Promise.reject(reason): 주어진 reason으로 거부된 새로운 Promise 객체를 반환한다.
  • Promise.resolve(value): 주어진 값으로 해결되는 새로운 Promise 객체를 반환한다.

function getPig() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🐷");
    }, 1000);
  });
}

function getChicken() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🐔");
    }, 3000);
  });
}

function getTiger() {
  return Promise.reject(new Error("no tiger"));
}

// 돼지와 닭을 같이 가지고 오기
getPig() //
  .then((pig) =>
    getChicken() //
      .then((chicken) => [pig, chicken])
  )
  .then(console.log);

// Promise.all은 병렬적으로 한번에 모든 Promise들을 실행!
Promise.all([getPig(), getChicken()]).then((animals) =>
  console.log("all", animals)
);

// Promise.race 주어진 Promise중에 제일 빨리 수행된 것이 이김!
Promise.race([getPig(), getChicken()]).then((animals) =>
  console.log("race", animals)
);

Promise.all([getPig(), getChicken(), getTiger()])
  .then((animals) => console.log("all error", animals))
  .catch(console.log);

Promise.allSettled([getPig(), getChicken(), getTiger()])
  .then((animals) => console.log("all settle", animals))
  .catch(console.log);

async/await

async function은 async 키워드로 선언된 함수이며 async 함수 안에 await 키워드가 허용된다.

-> async와 await를 사용하면 Promise 체이닝 방식을 명시적으로 구성할 필요가 없이 비동기식 Promise 기반 동작을 더 깔끔한 스타일로 작성할 수 있다.
-> async function의 return 값은 async function에 의해 반환된 resolve되는 값이거나 async function 내에서 발생하는 catch되지 않는 예외로 reject되는 값인 Promise이다.

async function에는 await 표현식이 포함될 수 있는데, 이 표현식은 async 함수의 실행을 일시 중지하고 전달된 Promise의 해결을 기다린 다음 async function 함수의 실행을 다시 시작하고 완료후 값을 반환한다.

function getBanana() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🍌");
    }, 1000);
  });
}

function getApple() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🍎");
    }, 3000);
  });
}

function getOrange() {
  return Promise.reject(new Error("no orange"));
}

// 바나나와 사과를 같이 가지고 오기
async function fetchFruits() {
  const banana = await getBanana();
  const apple = await getApple();
  return [banana, apple];
}

fetchFruits()
  .then((fruits) => console.log(fruits));

// async 함수를 Promise instance 메서드인 then()을 사용하여 fulfilled된 Promise의 반환값을 출력한다.
// async 함수 내부에서는 await 표현식을 이용해 Promise객체를 반환하는 비동기 작업들을
// 마치 동기적으로 작동하는 것과 같이 코드가 작성돼고 실행되는 것을 볼 수 있다.

0개의 댓글