[JS] 비동기 - Promise와 fetch API, Event Loop

강의 출처 : The Complete JavaScript Course 2022 Jonas (Udemy)

Promises 와 Fetch API

XMLHttpRequest로 사이트 받는 형태 
  const request = new XMLHttpRequest();
  request.open('GET', `https://restcountries.com/v2/name/${country}`);
  request.send();

Fetch API

const request = fetch('https://restcountries.com/v2/name/portugal');
console.log(request); // return promise

fetch function은 바로 promise를 return한다는 특징을 가지고 있다.

Promise

Promise : Promise 객체는 동기 작업으로 인한 미래의 성공 또는 실패와 그 결과 값을 나타낸다. 프로미스가 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자로, 비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기를 연결할 수 있다. 프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다. 다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(프로미스)을 반환한다.

간단히 말하면 비동기적으로 전달된 값들을 갖고 있는 container이다. 또는 미래의 값에 대한 container라고 할 수도 있다.

미래의 값(future value) 예시 : Response from AJAX call
처음 response 만들었을 때는 value 가 없지만 나중에 생길 것을 우리는 알 수 있다.

Promise 장점

  • 이벤트나 콜백함수 없이도 비동기적인 업무처리가 가능하다.
    We no longer need to rely on events and callbacks passed into asynchronous functions to handle asynchronous results
  • nested callbacks 대신에 promise chaining을 할 수 있다. (callback hell을 피할 수 있음)

Promise 에 대한 비유
Promise는 복권과도 같다. 만약 내가 올바른 결과값을 예상한다면 돈을 받을 것이고 그렇지 못한다면 받지 못함. (promise 즉 약속된 것이므로) 복권이 추첨되는 것은 비동기적으로 일어나는 것임.
lottery ticket that I will receive money if I guess correnct outcome

The Promise lifecycle

  1. Pending(Before the future value is available)
    --Async Task-->
  2. Settled (Asynchronous task has finished) ---> fullfilled(success! The value is now available) or rejected (an error happened)
    두 다른 상황의 promise 상태 다룰 수 있어야 한다.

우리가 이미 promise를 가지고 있을때 promise를 이용할 수 있다. ex) promise returned from Fetch API

Consuming Promises

아래 코드에서 response의 body에 접근하기 위해서는 json을 이용해야한다. json은 또다른 promise를 return한다는 특징을 가지고 있다.

const renderCountry = function (data, className = '') {
  const html = ` <article class="country ${className}">
    <img class="country__img" src="${data.flag}" />
    <div class="country__data">
      <h3 class="country__name">${data.name}</h3>
      <h4 class="country__region">${data.region}</h4>
      <p class="country__row"><span>👫</span>${(
        +data.population / 10000000
      ).toFixed(1)} people</p>
      <p class="country__row"><span>🗣️</span>${data.languages[0].name}</p>
      <p class="country__row"><span>💰</span>${data.currencies[0].name}</p>
    </div>
  </article>`;
  countriesContainer.insertAdjacentHTML('beforeend', html);
  countriesContainer.style.opacity = 1;
};

const renderError = function (msg) {
  countriesContainer.insertAdjacentText('beforeend', msg);
  countriesContainer.style.opacity = 1;
};
const getCountryData = function (country) { 
  fetch(`https://restcountries.com/v2/name/${country}`)
    .then(response => response.json())
    .then(data => renderCountry(data[0]));
};
getCountryData('portugal');

Chaining Promises

& Handling Rejected Promise

const getCountryData = function (country) {
  //country 1
  fetch(`https://restcountries.com/v2/name/${country}`)
    .then(response => {
      //Throwing errors manually
      if (!response.ok) throw new Error(`Country not found ${response.status}`);

      return response.json();
    })
    .then(data => {
      renderCountry(data[0]);
      const neighbor = data[0].borders[0];
      if (!neighbor) return;

      //country 2
      return fetch(`https://restcountries.com/v2/alpha/${neighbor}`);
    })
    .then(response => {
      if (!response.ok) throw new Error(`Country not found ${response.status}`);
      return response.json();
    })
    .then(data => renderCountry(data, 'neighbour'))
    .catch(err => {
      console.error(`${err}💥💥💥`); //Failed to fetch💥💥💥
      renderError(`Something went wrong 💥💥 ${err.message}. Try again!`); //user들도 화면에서 볼 수 있도록
    })
    .finally(() => {
      countriesContainer.style.opacity = 1;
    });
};

getJSON이라는 함수를 만들어서 코드를 더욱 간편하게 만들기

const getJSON = function (url, errorMsg = 'Something went wrong') {
  return fetch(url).then(response => {
    if (!response.ok) throw new Error(`${errorMsg} (${response.status})`);

    return response.json();
  });
};

const getCountryData = function (country) {
  //country 1
  getJSON(`https://restcountries.com/v2/name/${country}`, 'Country not found')
    .then(data => {
      renderCountry(data[0]);
      const neighbor = data[0].borders[0];
      if (!neighbor) throw new Error('No neighbor found!');

      //country 2
      return getJSON(
        `https://restcountries.com/v2/alpha/${neighbor}`,
        'Country not found'
      );
    })
    .then(data => renderCountry(data, 'neighbour'))
    .catch(err => {
      console.error(`${err}💥💥💥`); //Failed to fetch💥💥💥
      renderError(`Something went wrong 💥💥 ${err.message}. Try again!`); //user들도 화면에서 볼 수 있도록
    })
    .finally(() => {
      countriesContainer.style.opacity = 1;
    });
};

btn.addEventListener('click', function () {
  getCountryData('portugal');
});

getCountryData('dfasdfadsf'); 
// 찾을 수 없는 값을 넣을 경우 promise는 reject로 인식하지 않는다. promise는 오직 internet connection이 되지 않았을 때만 reject로 인식!  error를 undefined (reading 'flag')라고 표현. That's not what we want.

수동적으로 error 처리를 해줄 때에는 throw new Error('')해줄 것!

Asynchronous Behind the Scenes : The Event Loop

Runtime in the Browser : 'Container' which includes all the pieces necessary to execute JavaScript code
JS engine : "Heart of the runtime"
Heap : Where object are stored in memory
Call stack : Where code is actually executed -> only ONE thread of execution. No multitasking!
WEB APIs : APIs privided to the engine(JS 내에 있는 것 아님!)
Callback Queue : Ready to be executed callback functions(coming from events)
Concurrency model : How JavaScript handles multiple tasks happening at the same time

How Asynchronous JavaScript Works Behind the scenes

el = documemt.querySelector('img');
el.src = 'dog.jpg'; 
el.addEventListner('load',()=> {
el.classList.add('fadeIn');
});
fetch('http://someurl.com/api')
 .then(res => console.log(res))

'dog.jpg'는 call stack(main thread of execution) 내에서 이루지는 것이 아니라 web APIs environment 자체에서 이루어짐. => 비동기적 코드
WEB APIs : 비동기적인 업무가 처리되는 곳
이미지 로딩되는 동안 load event내에서 콜백함수 기다리고 있다. load가 끝나면 callback queue에 콜백함수 추가 된다.
data fetching 되는 동안 then 다음의 콜백함수 web APIs에서 기다리고 있다.
fetch가 끝나면 microtasks queue로 옮겨진다. (callback queue보다 우선순위에 있음. callback queue에 있는 콜백함수들보다 먼저 event loop가 데려간다. )

callback queue 내의 callback 함수는 event loop가 callstack으로 옮길 때까지 기다린다. 콜스택의 함수가 비어지면 바로 발생한다.

요약 : WEB APIs 와 event loop가 블로킹이 되지 않는 비동기적 코드 실행을 가능하게 만든다.

Building a Simple Promise

new Promise(executer function)

const lotteryPromise = new Promise(function (resolve, reject) {
  console.log('Lottery draw is happening 💎');
  setTimeout(function () {
    if (Math.random() >= 0.5) {
      resolve('You WIN 💰');
    } else {
      reject(new Error('You lost your money 💩'));
    }
  }, 2000);
});

lotteryPromise.then(res => console.log(res)).catch(err => console.error(err));

또다른 예시 - setTimeout premise화 하기

const wait = function (seconds) {
  return new Promise(function (resolve) {
    setTimeout(resolve, seconds * 1000);
  });
};
wait(2)
  .then(() => {
    console.log('1 second passed');
    return wait(1);
  })
  .then(() => {
    console.log('2 second passed');
    return wait(1);
  })
  .then(() => {
    console.log('3 second passed');
    return wait(1);
  })
  .then(() => {
    console.log('4 second passed');
    return wait(1);
  });


콜백지옥과 비교
setTimeout(() => {
  console.log('1 second passed');
  setTimeout(() => {
    console.log('2 seconds passed');
    setTimeout(() => {
      console.log('3 seconds passed');
    }, 1000);
    setTimeout(() => {
      console.log('4 seconds passed');
    }, 1000);
  }, 1000);
}, 1000);

Promise resolve와 reject 함수 바로 만들어 실행하기

Promise.resolve('abc').then(x => console.log(x));
Promise.reject(new Error('Problem!')).catch(x => console.error(x));

Promisifying the Geolocation API

const getPosition = function () {
  return new Promise(function (resolve, reject) {
    // navigator.geolocation.getCurrentPosition(
    //   position => resolve(position),
    //   err => reject(err)
    // );
    navigator.geolocation.getCurrentPosition(resolve, reject);
  });
};

getPosition()
  .then(pos => console.log(pos))
  .catch(err => console.error(err));

const whereAmI = function () {
  getPosition()
    .then(pos => {
      const { latitude: lat, longitude: lng } = pos.coords;

      return fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`);
    })
    .then(response => {
      if (!response.ok)
        throw new Error(`Problem with geocoding ${response.status}`);
      return response.json();
    })
    .then(data => {
      //console.log(data);
      const { region, country } = data;
      console.log(`You are in ${region}`);
      return fetch(`https://restcountries.com/v2/name/${country}`);
    })
    .then(response => {
      if (!response.ok)
        throw new Error(`Country not found (${response.status})`);

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

좋은 웹페이지 즐겨찾기