비동기 함수를 사용하면 마치 동기 함수인 것처럼 프로미스 기반 코드를 작성할 수 있습니다.
비동기 함수는 Chrome, Edge, Firefox, Safari에서 기본적으로 사용 설정되며 솔직히 놀랍습니다. 이 함수를 사용하면 기본 스레드를 차단하지 않고도 동기식인 것처럼 promise 기반 코드를 작성할 수 있습니다. 이러한 함수를 사용하면 비동기 코드가 덜 '똑똑'해지고 가독성이 높아집니다.
비동기 함수는 다음과 같이 작동합니다.
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
함수 정의 앞에 async
키워드를 사용하는 경우 함수 내에서 await
를 사용할 수 있습니다. 프로미스를 await
하면 함수는 프로미스가 결정될 때까지 비차단 방식으로 일시중지됩니다. 프로미스가 충족되면 값을 반환합니다. 프로미스가 거부되면 거부된 값이 발생합니다.
브라우저 지원
예: 가져오기 로깅
URL을 가져오고 응답을 텍스트로 기록한다고 가정해 보겠습니다. 프로미스를 사용하는 방법은 다음과 같습니다.
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
다음은 비동기 함수를 사용하는 동일한 결과입니다.
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
줄 수는 동일하지만 모든 콜백이 사라졌습니다. 이렇게 하면 특히 Promise에 익숙하지 않은 경우 훨씬 쉽게 읽을 수 있습니다.
비동기 반환 값
비동기 함수는 await
사용 여부와 관계없이 항상 프로미스를 반환합니다. 이 프로미스는 비동기 함수가 반환하는 것과 함께 해결되거나 비동기 함수에서 발생한 것과 함께 거부됩니다. 다음과 같습니다.
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
hello()
를 호출하면 "world"
로 실행되는 프로미스가 반환됩니다.
async function foo() {
await wait(500);
throw Error('bar');
}
foo()
를 호출하면 Error('bar')
와 함께 거부되는 프로미스가 반환됩니다.
예: 응답 스트리밍
비동기 함수의 이점은 복잡한 예에서 더욱 두드러집니다. 청크를 로그아웃하는 동안 응답을 스트리밍하고 최종 크기를 반환한다고 가정해 보겠습니다.
다음은 프로미스를 사용한 예입니다.
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 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 루프로 바뀌었습니다. 훨씬 낫습니다. 향후에는 비동기 반복기를 사용할 수 있습니다. 이는 while
루프를 for-of 루프로 대체하여 더 깔끔해집니다.
기타 비동기 함수 구문
async function() {}
는 이미 살펴봤지만 async
키워드는 다른 함수 문법과 함께 사용할 수 있습니다.
화살표 함수
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
객체 메서드
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(…);
단, 지나치게 순차적으로 작성해서는 안 됩니다.
동기식 코드를 작성하더라도 병렬로 작업할 수 있는 기회를 놓치지 마세요.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
위의 작업을 완료하는 데 1,000ms가 걸리지만,
async function parallel() {
const wait1 = wait(500); // Start a 500ms timer asynchronously…
const wait2 = wait(500); // …meaning this timer happens in parallel.
await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
return 'done!';
}
위의 작업은 두 대기가 동시에 발생하므로 완료하는 데 500ms가 걸립니다. 실제 예를 살펴보겠습니다.
예: 가져오기를 순서대로 출력하기
일련의 URL을 가져와서 최대한 빨리 올바른 순서로 로깅한다고 가정해 보겠습니다.
심호흡 - 프로미스와 함께 사용할 때의 결과는 다음과 같습니다.
function markHandled(promise) {
promise.catch(() => {});
return promise;
}
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map((url) => {
return markHandled(fetch(url).then((response) => response.text()));
});
// log them in order
return textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text));
}, Promise.resolve());
}
네, 맞습니다. reduce
를 사용하여 일련의 프로미스를 체이닝하고 있습니다. 저는 정말
똑똑해요. 하지만 이 방법은 스마트한 코딩을 사용하지 않는 것이 낫습니다.
그러나 위의 함수를 비동기 함수로 변환할 때는 너무 순차적으로 처리하고 싶을 수 있습니다.
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }
function markHandled(...promises) { Promise.allSettled(promises); } async function logInOrder(urls) { // fetch all the URLs in parallel const textPromises = urls.map(async (url) => { const response = await fetch(url); return response.text(); }); markHandled(...textPromises); // log them in sequence for (const textPromise of textPromises) { console.log(await textPromise); } }
브라우저 지원 해결 방법: 생성기
생성기를 지원하는 브라우저(모든 주요 브라우저의 최신 버전 포함)를 타겟팅하는 경우 비동기 함수를 폴리필(polyfill)할 수 있습니다.
Babel이 이 작업을 자동으로 수행합니다. Babel REPL을 통한 예시는 다음과 같습니다.
- 트랜스파일된 코드가 얼마나 유사한지 확인합니다. 이 변환은 Babel의 es2017 사전 설정에 포함되어 있습니다.
대상 브라우저에서 비동기 함수를 지원하면 사용 중지할 수 있으므로 트랜스파일 방식을 사용하는 것이 좋습니다. 그러나 실제로 트랜스파일러를 사용하고 싶지 않다면 Babel의 polyfill을 직접 사용하면 됩니다. 다음을 대신해서 사용합니다.
async function slowEcho(val) {
await wait(1000);
return val;
}
polyfill을 포함하고 다음과 같이 작성하면 됩니다.
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
생성기 (function*
)를 createAsyncFunction
에 전달하고 await
대신 yield
를 사용해야 합니다. 그 외에는 동일하게 작동합니다.
해결 방법: 재생기
이전 브라우저를 타겟팅하는 경우 Babel은 생성기도 트랜스파일할 수 있으므로 IE8까지 비동기 함수를 사용할 수 있습니다. 이렇게 하려면 Babel의 es2017 사전 설정 및 es2015 사전 설정이 필요합니다.
출력이 그렇게 깔끔하지 않으므로 코드가 팽창되지 않도록 주의하세요.
모든 것을 비동기화하세요!
모든 브라우저에서 비동기 함수가 지원되면 promise를 반환하는 모든 함수에 비동기 함수를 사용하세요. 코드를 더 깔끔하게 만들 뿐만 아니라 함수가 항상 프로미스를 반환하도록 합니다.
저는 2014년에 비동기 함수를 사용하게 되어 매우 기뻤는데, 브라우저에서 실제로 이러한 함수가 나오게 되어 기쁩니다. 와!