원글
Jake Archibald, https://developers.google.com/web/fundamentals/primers/async-functions
async 함수는 크롬55 버전부터 사용할 수 있다. 이 함수는 프로미스 기반의 코드들을 메인 스레드의 블로킹 없이 동기화 형식으로 사용할 수 있게 한다. 비동기 코드를 "덜 영리하게"하고 읽기 쉽게 만들어준다.
async 함수는 다음과 같이 사용한다.
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
}
catch (rejectedValue) {
// …
}
}
async
키워드를 함수 정의 앞에 사용한다면, 함수 내부에서 await
라는 키워드를 사용할 수 있다. 프로미스를 await
하고 있을 때, 그 함수는 논-블로킹 상태에서 프로미스가 해결(resolve 또는 reject)될 때까지 일시 정지 상태가 된다. 그리고 프로미스가 완료된 후 그 값을 돌려 받는다. 만약 프로미스가 거절되면 해당 값(또는 에러)이 던져(throw)진다.
URL을 fetch하고, 응답 텍스트를 로깅 한다 가정해보자. 프로미스를 사용하는 방식은 다음과 같다.
function logFetch(url) {
return fetch(url)
.then(response => response.text())
.then(text => {
console.log(text);
})
.catch(err => {
console.error('fetch failed', err);
});
}
그리고 다음은 async 함수를 사용한 예시다.
async function logFetch(url) {
try {
const response = await fetch(url);
const text = await response.text();
console.log(text);
}
catch (err) {
console.log('fetch failed', err);
}
}
라인 수는 거의 비슷하지만, 모든 callback이 사라졌다. 특히 프로미스에 덜 익숙한 사람들에게 가독성이 높아졌다.
참고:
await
로 기다리는 모든 것들을Promise.resolve()
로 처리하기 때문에 꼭 기존 함수의 반환 값이 프로미스가 아니어도 괜찮다.
async 함수들은 await
와 별개로 항상 프로미스를 반환한다. async 함수가 반환하는 것을 항상 resolve
로 처리하고, 에러는 reject
로 처리한다.
// wait ms milliseconds
function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
hello()
호출은 Promise.resolve('world')
를 반환한다.
async function foo() {
await wait(500);
throw Error('bar');
}
foo()
호출은 Promise.reject(Error('bar'))
를 반환한다.
더욱 복잡한 예시에서 async 함수의 이점이 돋보인다. 스트리밍 응답에서 청크를 로깅하고 마지막에 최종 크기를 반환한다고 가정해 보자.
다음은 프로미스 기반의 코드이다.
function getResponseSize(url) {
return fetch(url).then(response => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
})
});
}
비동기 루프를 수행하기 위해 processResult
함수를 재귀호출하였다. 저렇게 작성한 코드는 스스로가 "매우 똑똑"하다고 느끼게 한다. 그러나 대부분의 "똑똑한" 코드는, 이해하기 위해서 마치 90년대의 매직아이 그림처럼 꽤 오랫동안 지켜봐야 한다.
이제 async 함수를 사용해보자.
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
모든 "똑똑함"은 사라졌다. 이제 스스로 똑똑하고 잘난 체하게 만들었던 비동기 루프를 믿을만하면서 지루한 while-loop로 변경했다. 이게 훨씬 낫다. 앞으로는 while-loop를 for-of 루프로 대체하는 async iterators를 사용하여 더 깔끔하게 만들 것이다.
지금까지 async function() {}
문법에 대해서만 살펴보았지만 async
키워드를 다른 함수 문법들에도 사용할 수 있다.
// map some URLs to json-promises
const jsonPromises = urls.map(async url => {
const response = await fetch(url);
return response.json();
});
참고:
array.map(func)
는 async 함수를 따로 고려하지 않는다. 즉,map
이후 배열의 원소는 모두 프로미스 객체이며, 각 이터레이션은 그 전 이터레이션의 수행이 끝날 때까지 기다리지 않는다.
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jaffathecake').then(...);
참고: 클래스 생성자와 getter/setter는 async가 될 수 없다.
비동기를 동기 코드처럼 작성할 수 있지만, 병렬 처리를 할 수 있다는 것은 잊지 말자.
async function series() {
await wait(500);
await wait(500);
return "done!";
}
series
함수는 완료까지 1000ms가 걸린다.
async function parallel() {
const wait1 = wait(500);
const wait2 = wait(500);
await wait1;
await wait2;
return "done!";
}
parallel
함수는 완료까지 500ms가 걸린다. 각 대기 시간은 차례대로 수행되는 게 아니라 같은 시간에 병렬로 수행되기 때문이다.
일련의 URL들을 가능한 한 빠르게 fetch 하여 순서대로 로깅 한다고 가정해보자.
일단 먼저 심호흡 한번 하고 프로미스 기반의 코드를 보자.
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// log them in order
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}
reduce
를 사용해서 프로미스 배열을 체이닝 하였다. 매우 "똑똑"하다. 그렇지만 이건 "너무 똑똑"하고, 우리는 이것 없이 더 잘 살 수 있다.
그러나 위의 프로미스 기반의 코드를 async 함수로 변환할 때, 지나치게 순차적인 경우가 있다.
비추천
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
꽤 깔끔해 보이지만 두 번째 fetch는 첫 번째 fetch 응답의 text()
를 다 처리하기 전까지 시작되지 않는다. 이 코드는 병렬로 fetch를 수행하는 위의 promise 기반의 코드보다 느리다. 하지만 고맙게도 이상적인 중간 지점이 있다.
추천
async function logInOrder(urls) {
// fetch all the URLs in parallel
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// log them in sequence
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
urls
에서 병렬로 fetch와 text()
를 수행한다. 그리고 "똑똑한" reduce
를 지루하지만 읽기 편한 표준 for-loop로 대체했다.
지금 이 글을 쓰고 있는 시점에, 크롬55에서는 async함수를 바로 사용할 수 있지만, edge, firefox, safari 브라우저들은 아직 반영되지 않았다.
개발하고자 하는 애플리케이션의 지원 브라우저가 제너레이터를 지원한다면 (최신 버전의 메이저 브라우저들은 모두 제너레이터를 지원한다.) async 함수들을 일종의 폴리필처럼 사용할 수 있다.
그리고 Babel이 이 작업을 해줄 것이다. 바벨의 REPL 예시를 통해 변환된 코드가 원래 코드와 얼마나 유사한지도 확인해보자. 이 변환은 babel-preset-es2017의 일부이다.
애플리케이션의 지원 브라우저가 async 함수를 지원한다면 쉽게 기존 변환을 끌 수 있으므로 트랜스파일 방식을 추천한다. 그러나 정말 트랜스파일러를 사용하기 싫다면 Babel의 polyfill을 사용해 다음과 같은 코드를 작성할 수 있다.
기존 async-await으로 작성
async function slowEcho(val) {
await wait(1000);
return val;
}
polyfill을 사용하여 작성
const slowEcho = createAsyncFunction(function*(val) {
yield wait(1000);
return val;
});
createAysncFunction
에 인자로 제너레이터(function*
)를 넘겼으며, await
대신에 yield
를 사용했다는 것을 참고하자. 이 외에는 모두 같다.
만약 구버전 브라우저들이 지원 범위에 있다면, 바벨은 제너레이터 또한 트랜스파일 할 수 있으며, async 함수들을 IE8에서까지 사용할 수 있다. 이를 위해서는 babel-preset-es2017과 babel-preset-es2015이 필요하다.
변환된 코드는 썩 깔끔하지 않기 때문에 코드가 많아지는 것에 주의하자.
async 함수가 모든 브라우저에 사용되면 프로미스를 반환하는 모든 함수에서 사용하자. 코드를 더 가볍게 만들뿐만 아니라, 함수가 항상 프로미스를 반환한다는 것을 보장한다.