Asynchronous

하나의 코드의 흐름과는 별개의 코드의 흐름을 만들어 여러가지 작업을 동시에 할 수 있게 하는 것.

좋은 service를 위해선 필수적이다.

AJAX

Asynchronous Javascript And Xml의 약자

Allows us to communicate with remote web server in an asynchronous way. with AJAX calls, we can request data from web service dynamically.

API?

Application Programming Interface

Piece of software that can be used by another piece of software, in order to allow applications to talk to each other

Promises

Definition : An object that is used as a placeholder for the future result of an asynchronous operation (A container for an asynchronously delivered value, A container for a future value)

여기서 future value = Response from AJAX call 라고 생각하면 된다.

Callback function 들이 겹겹히 쌓여 Callback Hell이 되는것을 해결해주는 ES6에서 추가된 기능이다.

Callback Hell?

//"Callback Hell"
//cascade하게 function이 한번에 모두 실행되어야 할 때 이렇게 설계한다.
function f1(){
    function f2(){
        function f3(){

        }
    }
}

Promise의 장점

We no longer need to rely on events and callbacks passed into asynchronous functions to handle asynchronous results

Instead of nesting callbacks, we can chain promises for a sequence of asynchronous operations: escaping callback hell

Promise Lifecycle

Promises is only settled once! (Fullfilled or Rejected)

PromiseLifeCycle

Let’s Implement Promises

Callback Chain보다 직관적이고 읽기 쉽다.

//하나의 promise가 끝나고 다른 promise를 실행
//따라서 aka. "flat chain"
const getCountryData = function (country) {
    //country 1
    fetch('https://restcountries.eu/rest/v2/name/${country}')
        .then(response => response.json())
        .then(data => {
            renderCounrty(data[0]);
            const neighbour = data[0].borders[0];

            if (!neighbour) return;

            //country 2
            return fetch('https://restcountries.eu/rest/v2/name/${neighbour}');

            // 안좋은 예 Promise안에 Promise가 되므로 callback hell에 해당한다.
            //fetch('https://restcountries.eu/rest/v2/name/${neighbour}').then(response => response.json())
        })
        .then(response => response.json());
        .then(data => renderCountry(data, 'neighbour'));
}

getCounrtyData('portugal');

Handle Rejected Promises (Handle Errors)

  1. then method의 2번째 parameter에 error handle callback function implement
  2. catch function을 chain 안에 추가한다. promise가 rejected이면 어디서 error가 발생하든 발동하는 것이 특징

java의 try…catch…finally와 유사하다.

const getCountryData = function (country) {
    //country 1
    fetch('https://restcountries.eu/rest/v2/name/${country}')

        
        .then(response => {
            console.log(response);

            //How to Throw Error Manually no.1
            if(!response.ok)
                //Throw시에 현재 method가 바로 종료하고 Rejected promise를 return = catch method 발동
                throw new Error('Country not found ${response.status}');

            return response.json()}, 
        //How to catch Err no.1
        //then method에 두번째 parameter추가하기
        err => alert(err))
        .then(data => {
            renderCounrty(data[0]);
            const neighbour = data[0].borders[0];

            if (!neighbour) return;

            //country 2
            //chaining promise
            return fetch('https://restcountries.eu/rest/v2/name/${neighbour}');
        })
        .then(response => response.json());
        .then(data => renderCountry(data, 'neighbour'));

        //How to catch Err no.2
        //err에는 JS built-in error object가 들어온다.
        .catch(err => {
            console.error('${err} !!!!!!!');
            alert('${err.message}');
        });

        //Promise가 fullfilled이건 reject이건 반드시 발동하는 method
        .finally(() => {
            
        })
}

getCounrtyData('portugal');

How Asyncronous works behind the scene???

Runtime? : 자바스크립트 코드를 실행하기 위한 모든 부품들의 모임 즉, 실행환경

JSRuntimeInBrowser

JS engine = Heap + Call stack

Heap : Where Objects are stored in memory

Call stack : Where code is actually executed, Only ONE!!!! thread of execution. NO Multitasking!!

Web APIs : APIs provided to the engine, JS의 파트가 아니다. (DOM, Fetch API, Timers, Geolocation API 등등…)

Callback Queue : Ready-to-be-executed callback functions(coming from events)

Event Loop : When ‘Call stack’ is empty, event loop takes callbacks from the callback queue and puts them into call stack so that they can be executed, Asyncronous하게 움직이도록 하는데에 핵심이다.

JS엔진은 Single Thread인데 어떻게 Asyncronous하게 움직일 수 있지?

web API, callback queue(or microtasks queue), event loop가 Async를 가능하게 해준다.

call stack 이외에 Fetch(), setTimeOut(), DOM() 등을 제공하는 webAPIs에서 개별적으로 처리가 진행이 된다. (DOM은 Async하게 움직이지 않고 바로 callback queue에 추가된다.)

만약 image load처리가 있다고 가정한다면, image load가 끝나면 load event가 발동되고 이를 통해 callback queue에 추가 된다.

callback queue에서 순서를 기다리고 call stack에 실행되고 있는 코드가 없다면

Event Loop를 통해 image load 이벤트가 callback queue에서 call stack으로 올라가 실행된다.(aka. “Event Loop tick”)

즉, Event loop가 어떤 코드를 실행시킬지 정하기 때문에 Async환경을 만드는데 가장 중요하다.

  • promise

promise는 일반 event와는 조금 다르다.

promise들은 callback queue가 아닌 microtasks queue를 사용하며 callback queue보다 우선 실행된다.

//1
console.log('Test start');
//2
setTimeout(() => console.log('0 sec timer'), 0);
//3
//Promise를 만들어 즉시 resolve한다.
Promise.resolve('Resolved promise 1').then(res => console.log(res)); //expecting result : "Resolved promise 1"

//4
Promise.resolve('Resolved promise 2').then(res => {
    for (let i = 0; i < 10000000000; i++) {}
    console.log(res);
}); //expecting result : "Resolved promise 1"
//5
console.log('Test end');

//출력 순서 1 -> 4 -> 3 -> 2
//call back이외의 code들이 가장 먼저 실행된다. Synchronous한 1,5가 가장 먼저 실행된다.
//promise는 microtasks queue로 인해 가장 먼저 실행된다. 따라서 3,4
//callback queue에 해당하는 2는 가장 마지막

Promise

JS의 특별한 Object인 Promise에 대해

constructor

promise의 constructor는 오직 하나의 argument만 받는다

aka. “executor function”

encapsulate other async behavior

//aka. executor function
const lotteryPromise = new Promise(function(resolve, reject){
    console.log('Lottery draw is happening');
    
    //setTimeout을 promise안에 넣어서 일반 callback function을 promise로 다루도록 하였다.
    //다른 Asynchronous behavior 또한 이렇게 promise안에 encapsulate할 수 있다. aka.promisifying
    setTimeout(function(){
        if(Math.random() >= 0.5){
            //when it's fullfilled(aka. resolve promise)
            //resolve function이 return 하는 것이 훗날 then method가 받는 parameter가 된다.
            resolve('YOU WIN!!!!!!');
        }else{
            //when it's rejected
            //catch handler가 이 method를 통해 전달되어 실행된다.
            reject(new Error('YOU LOST.......... your money'));
        }
    }, 2000)
})

//이렇게 callback based async프로세스를 promise based async로 하는것을 'promisifying'이라고 한다
lotteryPromise
.then(res => console.log(res))
.catch(err => console.error(err));

//Promisifying setTimeout (원래 setTimeout은 일반 callback function이다)
const wait = function(seconds){
    //timer는 실패할 수가 없으므로 rejected는 안해줘도 된다.
    return new Promise(function(resolve){
        setTimeout(resolve, seconds * 1000);
    });
};

wait(2).then(() => {
    console.log('I waited for 2 seconds');
    //하나의 promise가 진행되고 연속으로 promise를 실행 시킨다.`
    return wait(1);
}).then(() => console.log('I waited for 1 seconds'));

Consuming Proromises with Async/Await (새로운 문법)

ES2017에 추가 된 새로운 문법이다. class처럼 문법만 바뀐거고 뒤의 처리는 promise와 같다.

Synthtic sugar!

//새로운 문법으로 똑같은 처리를 다시 씀
const whereAmI = async function () {
  //err handle : .catch가 없기 때문에 고전 문법인 try catch를 사용해 error handle
  try {
    /*  설명
    : 새로운 문법을 통해 여러개의 return을 필요로 하지 않아 깔끔하다.
    : 하나의 single stream처럼 표현할 수 있어 가독성 up

        async : 해당 function이 main thread와는 다른 stream이라는 것을 정해주는 것으로
            main stream을 block하지 않고 별개의 stream으로 처리가 이루어진다.
        await : 해당 function이 return할때까지 대기!
            function의 결과값이 다음번 처리에 필요할때 걸어주는 것.
    */

    // Geolocation
    const pos = await getPosition();
    const { latitude: lat, longitude: lng } = pos.coords;

    // Reverse geocoding
    const resGeo = await fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`);
    if (!resGeo.ok) throw new Error('Problem getting location data');

    const dataGeo = await resGeo.json();
    console.log(dataGeo);

    // Country data
    const res = await fetch(
      `https://restcountries.eu/rest/v2/name/${dataGeo.country}`
    );
    if (!resGeo.ok) throw new Error('Problem getting country');

    const data = await res.json();
    console.log(data);
    renderCountry(data[0]);

    // Async function return
    return `You are in ${dataGeo.city}, ${dataGeo.country}`;
  } catch (err) {
    console.error(`${err} 💥`);
    renderError(`💥 ${err.message}`);

    // Reject promise returned from async function
    throw err;
  }
};
whereAmI();

/*
    * async function의 return
    - 따로 catch안에 throw를 설정해주지 않으면 모두 fullfilled value로서 return된다.
    
    * main thread처리에 async의 return이 필요한 경우
    - async function이 return 될 때까지 멈춰있는다.
    ex) 
    console.log('1')
    const city = whereAmI();
    console.log(city)
    console.log('3')
    출력 : 1 -> Promise -> 3
*/

// 위 아래가 같은 처리이다.(문법만 다르게 함)
// whereAmI()
//   .then(city => console.log(`2: ${city}`))
//   .catch(err => console.error(`2: ${err.message} 💥`))
//   .finally(() => console.log('3: Finished getting location'));

(async function () {
  try {
    const city = await whereAmI();
    console.log(`2: ${city}`);
  } catch (err) {
    console.error(`2: ${err.message} 💥`);
  }
  console.log('3: Finished getting location');
})();

///////////////////////////////////
//위와 같은 처리(return과 throw제외)
const whereAmIOrigin = function () {
  getPosition()
    .then(pos => {
      const { latitude: lat, longitude: lng } = pos.coords;

      return fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`);
    })
    .then(res => {
      if (!res.ok) throw new Error(`Problem with geocoding ${res.status}`);
      return res.json();
    })
    .then(data => {
      console.log(data);
      console.log(`You are in ${data.city}, ${data.country}`);

      return fetch(`https://restcountries.eu/rest/v2/name/${data.country}`);
    })
    .then(res => {
      if (!res.ok) throw new Error(`Country not found (${res.status})`);

      return res.json();
    })
    .then(data => renderCountry(data[0]))
    .catch(err => console.error(`${err.message} 💥`));
};

whereAmIOrigin();

Promises in Parallel

여러 promises들을 동시에 처리 시키는 방법!

즉, Multi-Threading 방법이다.

Promise.all의 특성 : 여러 처리 중 하나라도 reject이면 모두 reject가 되어 return된다.

const get3Countries = async function (c1, c2, c3) {
  try {
    // * await가 걸려있으므로 순차적으로 처리 진행
    // const [data1] = await getJSON(
    //   `https://restcountries.eu/rest/v2/name/${c1}`
    // );
    // const [data2] = await getJSON(
    //   `https://restcountries.eu/rest/v2/name/${c2}`
    // );
    // const [data3] = await getJSON(
    //   `https://restcountries.eu/rest/v2/name/${c3}`
    // );
    // console.log([data1.capital, data2.capital, data3.capital]);

    //Promise.all을 통해 3개의 처리를 한꺼번에 진행
    const data = await Promise.all([
      getJSON(`https://restcountries.eu/rest/v2/name/${c1}`),
      getJSON(`https://restcountries.eu/rest/v2/name/${c2}`),
      getJSON(`https://restcountries.eu/rest/v2/name/${c3}`),
    ]);
    console.log(data.map(d => d[0].capital));
  } catch (err) {
    console.error(err);
  }
};
get3Countries('portugal', 'canada', 'tanzania');

Other promises combinator: race, allSettled and any

all 이외에 Promise를 동시에 진행시키는 Promise의 static method를 알아보자

Promise.race는 여러개의 Primise를 동시에 진행시켜 되는대로 return시키는 method로

그 특성을 이용해 일정 시간을 초과하면 reject하게 끔 할 수 있다.

// Promise.race
(async function () {
  const res = await Promise.race([
    getJSON(`https://restcountries.eu/rest/v2/name/italy`),
    getJSON(`https://restcountries.eu/rest/v2/name/egypt`),
    getJSON(`https://restcountries.eu/rest/v2/name/mexico`),
  ]);
  console.log(res[0]);
})();

const timeout = function (sec) {
  return new Promise(function (_, reject) {
    setTimeout(function () {
      reject(new Error('Request took too long!'));
    }, sec * 1000);
  });
};

// race의 특성을 이용한 시간초 예외처리
// 내용:URL을 요청했을때 5초보다 빠르면 fullfilled, 안되면 timeout으로 rejected를 반환
Promise.race([
  getJSON(`https://restcountries.eu/rest/v2/name/tanzania`),
  timeout(5),
])
  .then(res => console.log(res[0]))
  .catch(err => console.error(err));

Promise.allSettled는 ES2020에 추가된 method이다.

Promise.all과는 다르게 무조건 fullfilled 상태로 return된다.

하지만 각각 fullfilled인지 reject인지 처리별로 상태는 써져있다.

// Promise.allSettled
// reject가 있어도 모두 fullfilled로 반환된다.
Promise.allSettled([
  Promise.resolve('Success'),
  Promise.reject('ERROR'),
  Promise.resolve('Another success'),
]).then(res => console.log(res));

//처리 중에 reject가 있으므로 catch된다.
Promise.all([
  Promise.resolve('Success'),
  Promise.reject('ERROR'),
  Promise.resolve('Another success'),
])
  .then(res => console.log(res))
  .catch(err => console.error(err));

Promise.any는 ES2021에 추가된 method이다.

reject된 것은 무시하고 return하여 모두 fullfilled이다.

모든 처리가 reject면 reject를 반환한다.

// Promise.any [ES2021]
Promise.any([
  Promise.resolve('Success'),
  Promise.reject('ERROR'),
  Promise.resolve('Another success'),
])
  .then(res => console.log(res))
  .catch(err => console.error(err));