자바스크립트 비동기 [2] – Promise, 이벤트 루프

Promise와 Fetch API

const request = fetch(`https://restcountries.com/v3.1/name/portugal`);
console.log(request); // Promise
  • fetch를 통해 받은 응답은 Promise라는 형태로 출력된다. 프로미스는 비동기 동작의 결과물을 담고있는 객체이다.
  • 프로미스는 시퀀스를 위한 체이닝이 가능하므로 nested callback 대신 사용할 수 있다.
  • ES6부터 등장했다.

Promise lifecycle

  • 프로미스는 시간에 따라 상태가 변화하며, 이를 프로미스의 라이프사이클이라고 부른다.
  • 프로미스를 생성하는 것을 Build Promise(제작 코드), Comsume Promise(소비 코드)라고 부른다.

Consume promise(소비 코드)

// const getCountryData = function (country) {
//   fetch(`https://restcountries.com/v3.1/name/${country}`).then(function (
//     response
//   ) {
//     console.log(response);
//     return response.json();
//   }).then(function (data) {
//     console.log(data);
//     renderCountry(data[0]);
//   });
// }
const getCountryData = function (country) {
  fetch(`https://restcountries.com/v3.1/name/${country}`)
    .then(response => response.json())
    .then(data => renderCountry(data[0]));
}
  • then 메소드에는 프로미스가 fulfilled 상태가 되면 실행되는 콜백 함수를 작성해준다.
  • 응답은 json 메소드를 사용해서 자바스크립트의 데이터 타입으로 만들어서 사용한다.

Chaining promises

const getCountryData = function (country) {
  fetch(`https://restcountries.com/v3.1/name/${country}`)
    .then(response => response.json())
    .then(data => {
      renderCountry(data[0]);
      console.log(data[0]);
      const neighbour = data[0].borders[0];

      if (!neighbour) return;
      console.log(neighbour);
      return fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`);
    })
    .then(response => response.json())
    .then(data => {
      console.log(data)
      renderCountry(data[0], 'neighbour');
    });
}

getCountryData('portugal');
  • then 메소드는 콜백 함수에 리턴문을 명시하지 않아도 언제나 프로미스를 리턴하며, 리턴을 명시할 경우 프로미스가 fulfilled 값일 때만 반환한다.
  • 프로미스를 체이닝해서 순서대로 실행되도록 순서를 보장할 수 있다. 콜백을 nesting하는 것보다 훨씬 코드가 간결하고 이해하기 쉽다.
  • then 메소드 안에서 then을 사용하지 않도록 주의하자. callback hell과 똑같은 효과이다.

Rejected promises 다루기

// fetch(`https://restcountries.com/v3.1/name/${country}`)
//   .then(response => response.json(),
//     err => alert(err));

const getCountryData = function (country) {
  fetch(`https://restcountries.com/v3.1/name/${country}`)
    .then(response => response.json())
    .then(data => {
      renderCountry(data[0]);
      console.log(data[0]);
      const neighbour = data[0].borders[0];

      if (!neighbour) return;
      console.log(neighbour);
      return fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`);
    })
    .then(response => response.json())
    .then(data => {
      console.log(data)
      renderCountry(data[0], 'neighbour');
    })
    .catch(err => alert(err))
    .finally(() => {
      countriesContainer.style.opacity = 1;
    });
}
  • then 메소드의 두번째 인자로 에러가 발생하면 처리해줄 코드를 작성할 수 있다.
  • 혹은 체이닝의 가장 마지막에 catch 메소드를 사용해서 모든 에러에 사용할 코드를 작성할 수 있다.
  • 요청이 성공하든 실패하든 항상 실행이 보장되어야 할 코드는 finally 메소드 안에 작성한다. catch 메소드 역시 프로미스를 리턴하므로 체이닝이 가능한 것이다.
  • 404 코드의 경우 error로 인식하지 않으므로 catch문에 걸리지 않는 것을 주의하자.

의도적으로 에러를 발생시키기

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/v3.1/name/${country}`,
    'Country not found'
  )
    .then(data => {
      renderCountry(data[0]);
      const neighbour = data[0].borders[0];

      if (!neighbour) throw new Error('No neighbour found!');

      // Country 2
      return getJSON(
        `https://restcountries.com/v3.1/alpha/${neighbour}`,
        'Country not found'
      );
    })

    .then(data => renderCountry(data, 'neighbour'))
    .catch(err => {
      console.error(`${err} 💥💥💥`);
      renderError(`Something went wrong 💥💥 ${err.message}. Try again!`);
    })
    .finally(() => {
      countriesContainer.style.opacity = 1;
    });
};
  • reponse.ok가 false일 때 throw new Error() 를 사용해서 에러를 발생시킬 수 있다.
  • DRY 코드를 위해 getJSON 함수를 작성해서 재사용하면 좋다.

이벤트 루프(Event Loop)

  • 자바스크립트 엔진이 싱글 스레드에서 동작한다면 실제로 모든 작업들이 동시에 처리되지는 않는다. 비동기 작업들은 콜백큐에 등록되어 순차적으로 처리되게 된다.
    • 따라서 5초를 기다리는 타이머 작업은 정확한 시간을 보장하지 못한다. 큐에 작업들이 밀리고 있다면 5초가 지난 이후에 실행될 수 있다.
  • 코드가 실행되면 비동기로 동작하는 작업들이 콜백큐에 등록 된다.
  • 이벤트 루프는 콜스택과 콜백큐를 바라보면서 콜스택이 비어있으면 콜백큐에 있는 작업을 콜스택에 넣어 엔진이 작업을 처리하도록 한다.
  • 이벤트 루프는 자바스크립트 런타임을 오케스트레이션 하는 관리자 역할을 한다.
  • 프로미스의 작업들은 콜백큐에 등록되지 않고, 마이크로태스크큐에 등록된다. 마이크로태스크큐는 콜백큐보다 우선순위를 가져, 이벤트 루프는 언제나 콜백큐의 작업보다(먼저 등록되었다고 해도) 프로미스의 작업을 먼저 처리하도록 콜스택에 등록한다.
console.log('Test start');
setTimeout(() => console.log('0 sec timer'), 0);
Promise.resolve('Resolved promise 1').then(res => console.log(res));

Promise.resolve('Resolved promise 2').then(res => {
  for (let i = 0; i < 1000000000; i++) {}
  console.log(res);
});

console.log('Test end');
  • 코드가 한줄씩 실행되면서 Test start, Test end 출력이 실행된 후 마이크로태스큐에 등록된 Promise 작업이 실행, 마지막으로 콜백큐에 등록된 setTimeout 작업이 실행된다.

Promise in practice

const lotteryPromise = new Promise(function (resolve, reject) {
  console.log('Lotter 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));

const wait = function (seconds) {
  return new Promise(function (resolve) {
    setTimeout(resolve, seconds * 1000);
  });
};

wait(1)
  .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'));

// setTimeout(() => {
//   console.log('1 second passed');
//   setTimeout(() => {
//     console.log('2 seconds passed');
//     setTimeout(() => {
//       console.log('3 second passed');
//       setTimeout(() => {
//         console.log('4 second passed');
//       }, 1000);
//     }, 1000);
//   }, 1000);
// }, 1000);

Promise.resolve('abc').then(x => console.log(x));
Promise.reject(new Error('Problem!')).catch(x => console.error(x));
  • 콜백을 프로미스를 사용해서 비동기 작업으로 만드는 것을 ‘promisifying’이라고 한다.
  • 바로 실행이 종료된 프로미스의 abc, Error: Problem! 출력 작업이 먼저 실행된 후, wait(1) 내용이 출력된다. 그 후 2초가 지난 뒤 먼저 등록된 프로미스 작업인 lotteryPromise 내용이 출력되고, 이후에 wait(2), wait(3), wait(4)가 순차적으로 실행된다.

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));

const whereAmI = 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.com/v3.1/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} 💥`));
};

btn.addEventListener('click', whereAmI);
  • 여러 요소들이 있을 때 프로미스를 효율적으로 사용하는 것이 성능에 얼마나 중요할지… 배움의 길은 멀다. 😇

Leave a Reply

Your email address will not be published. Required fields are marked *