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);
여러 요소들이 있을 때 프로미스를 효율적으로 사용하는 것이 성능에 얼마나 중요할지… 배움의 길은 멀다. 😇
Related