
자바스크립트로 애플리케이션을 만들다 보면 외부에서 데이터를 가져와야 할 때가 있습니다. 하지만 외부에서 데이터를 가져오는 것은 시간이 걸리는 작업입니다.
일반적으로 사용하는 작성된 순서대로 동작하는 코드(동기적인 코드)로 외부에서 데이터를 가져오는 시간이 걸리는 작업을 하면 중간에서 코드가 멈춰버릴 수도 있습니다. (blocking code)
이런 문제를 해결하기 위해서 비동기 코드를 이용할 수 있습니다. 비동기 코드는 순차적으로 작동하는 것이 아니라 자바스크립트 엔진의 백그라운드에서 따로 처리를 해서 중간에 블로킹하지 않게 동작하도록(non-blocking) 합니다.
비동기 요청을 하기 위해 예전부터 사용하던 방법은 콜백 함수를 이용하는 것입니다. 콜백(callback) 함수를 이용하면 함수를 조건부로 실행하거나 실행하는 순서를 조작할 수 있습니다.
// click 이벤트가 있을때 호출됨.
btn.addEventListener('click', () => {
alert('You clicked me!');
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});
// 함수를 조건에 따라 다르게 실행할 수 있다.
function lotteryExample(successCallback, errorCallback) {
// Slow method that runs in the background
const success = winLottery()
if (success) {
successCallback()
} else {
errorCallback()
}
}
lotteryExample(() => {
console.log('Success')
}, () => {
console.error("Error")
})
*콜백이라고 해서 모두 비동기 함수가 아닙니다.
[1,2,3].map(element => element*2) // not asynchronous code
그러나 콜백을 여러 개 사용하면 코드가 복잡해집니다 이런 상황을 콜백 지옥에 빠졌다고 합니다. 이런 코드는 가독성이 좋지 않기 때문에 에러에 취약합니다.
//callback hell
chooseToppings(function(toppings) {
placeOrder(toppings, function(order) {
collectOrder(order, function(pizza) {
eatPizza(pizza);
}, failureCallback);
}, failureCallback);
}, failureCallback);
이런 콜백 지옥을 피하기 위해서 ES6부터 Promise를 도입했습니다. Promise가 동작하는 방식은 현실의 복권과 비슷합니다. 복권을 샀을 때 추첨 후 당첨이 되면 보상금을 받고 당첨이 되지 않으면 아무것도 받지 못하는 것처럼, Promise 또한 비동기 통신 후 결과에 따라서 성공이나 실패에 해당하는 매서드를 호출합니다.
function lotteryExample() {
return new Promise((resolve, reject) => {
// callback을 매개변수로 받음
const success = winLottery()
if (success) {
resolve()
} else {
reject()
}
})
}
const lottery = lotteryExample()
lottery.then(() => {
console.log('win the lottery')
}).catch(() => {
console.error("nothing")
})
정리하면 promise란 비동기 통신의 결괏값을 가지고 있는 객체입니다. 성공과 실패에 해당하는 콜백 함수를 인수로 받아온 다음, 비동기 통신이 성공한다면 then매서드로 넘어가고 실패하면 catch매서드로 넘어갑니다.
프로미스 체이닝
앞에서 본 피자를 주문하는 코드를 promise를 이용해서 리팩터링 해 보겠습니다.
chooseToppings()
.then(function(toppings) {
return placeOrder(toppings);
})
.then(function(order) {
return collectOrder(order);
})
.then(function(pizza) {
eatPizza(pizza);
})
.catch(failureCallback);
앞에서 본 콜백을 모두 프로미스로 대체했습니다 보이는 것처럼 훨씬 읽기가 수월해졌습니다. 앞서 말했듯이 프로미스의 이점은 복잡한 비동기 통신도 콜백 지옥에 빠지지 않고 위와 같이 간략하게 작성할 수 있게 만든 것입니다.
Promise를 이용하면 콜백 지옥에 빠지지 않고 복잡한 비동기 통신 해결 가능하다.
Promise를 대체하는 새로운 것이 Promise 코드를 읽기 편하게 감싸줍니다. async는 비동기 코드를 이용한다는 뜻이고 awiat은 Promise의 값이 반환될 때까지 기다린다는 뜻입니다.
const setTimeoutPromise = (delay) => {
return new Promise((resolve, reject) => {
if (delay < 0) return reject("Delay must be greater than 0")
setTimeout(() => {
resolve(`You waited ${delay} milliseconds`)
}, delay)
})
}
const asyncTimeout = async() => {
const msg1 = await setTimeoutPromise(1000)
console.log(msg1)
console.log("First Timeout")
const msg2 = await setTimeoutPromise(3000)
console.log(msg2)
console.log("Second Timeout")
}
asyncTimeout()
// You waited 1 second
// First Timeout
// You waited 3 seconds
// Second Timeout
코드를 실행시켜 보면 1초 후 msg1이 실행되고 console.log가 찍히고 3초 후 msg2가 실행된 후 console.log가 찍힙니다. 만약 프로미스 값을 받지 않고 콘솔 로그가 동작한다면 에러를 일으키겠지만 await키워드를 이용해서 프로미스가 결과를 반환할 때까지 기다린 후 실행되므로 정상적으로 동작합니다.
await키워드로 async함수 내부의 실행 순서를 제어할 수 있다.
Promise체 이닝 또한 async/await 문법을 이용하면 더 편하게 이용할 수 있습니다.
//promise chianing을 위한 함수들
function getUserPreferences() {
const preferences = new Promise((resolve, reject) => {
resolve({
theme: 'dusk',
});
});
return preferences;
}
function getMusic(theme) {
if (theme === 'dusk') {
return Promise.resolve({
album: 'music for airports',
});
}
return Promise.resolve({
album: 'kind of blue',
});
}
function getArtist(album) {
return Promise.resolve({
artist: 'Brian Eno',
});
}
// Promise 체이닝
getUserPreferences()
.then(preferences => {
return getMusic(preferences.theme)
})
.then(music => {
return getArtist(music.album)
})
.then(album => {
console.log(album.artist) // 'Brian Eno'
})
// async/await
const asyncGetArtistByPreference = async() => {
const { theme } = await getUserPreferences();
const { album } = await getMusic(theme);
const { artist } = await getArtist(album);
return artist
}
asyncGetArtistByPreference().then(artist => {
console.log(artist) // 'Brian Eno'
})
async/await을 이용하면 기존의 동기식 코드를 짜듯이 Promise를 이용할 수 있습니다. Promise를 이용하면 콜백보다 읽기 편한 코드를 쓸 수 있지만 async/await을 이용하면 프로미스 코드도 개선 가능합니다.
결론: Promise를 이용해서 기존의 callback을 이용한 비동기 통신을 대체할 수 있고 Async/await를 통해서 Promise를 더 편하게 이용할 수 있다.
참고자료:
Graceful asynchronous programming with Promises -Mdn
The Complete JavaScript Promise Guide
Better Than Promises - JavaScript Async/Await
자바스크립트 코딩의 기술 -9장
The complete javascript course -Jonas Schmedtmann-