비동기 함수를 사용하면 약속 기반 코드를 동기 코드인 것처럼 작성할 수 있습니다.
비동기 함수는 Chrome, Edge, Firefox, Safari에서 기본적으로 사용 설정되며 솔직히 훌륭합니다. 이를 사용하면 동기 코드인 것처럼 약속 기반 코드를 작성할 수 있지만 기본 스레드를 차단하지 않습니다. 이를 사용하면 비동기 코드가 덜 '영리'해지고 더 읽기 쉬워집니다.
비동기 함수는 다음과 같이 작동합니다.
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
함수 정의 앞에 async
키워드를 사용하면 함수 내에서 await
를 사용할 수 있습니다. Promise를 await
하면 Promise가 해결될 때까지 함수가 비차단 방식으로 일시중지됩니다. 프로미스가 처리되면 값을 다시 가져옵니다. 프로미스가 거부되면 거부된 값이 발생합니다.
브라우저 지원
예: 가져오기 로깅
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);
}
}
줄 수는 동일하지만 모든 콜백이 사라졌습니다. 이렇게 하면 특히 약속에 익숙하지 않은 사용자도 훨씬 쉽게 읽을 수 있습니다.
비동기 반환 값
비동기 함수는 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')
로 rejects하는 프로미스가 반환됩니다.
예: 응답 스트리밍
비동기 함수의 이점은 더 복잡한 예시에서 더욱 커집니다. 청크를 로깅하는 동안 응답을 스트리밍하고 최종 크기를 반환하려고 한다고 가정해 보겠습니다.
프라미스를 사용하면 다음과 같이 작성할 수 있습니다.
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); } }
reduce
비트가 읽기 쉬운 지루한 표준 for 루프로 대체됩니다.
브라우저 지원 해결 방법: 생성기
생성자를 지원하는 브라우저 (모든 주요 브라우저의 최신 버전 포함)를 타겟팅하는 경우 비동기 함수를 일종의 폴리필할 수 있습니다.
Babel이 이를 대신해 줍니다. Babel REPL을 통한 예시
- 변환된 코드가 얼마나 유사한지 확인합니다. 이 변환은 Babel의 es2017 사전 설정의 일부입니다.
타겟 브라우저에서 비동기 함수를 지원하면 전환을 사용 중지할 수 있으므로 전환 접근 방식을 사용하는 것이 좋습니다. 하지만 정말로 전환 도구를 사용하고 싶지 않다면 Babel의 폴리필을 가져와 직접 사용하면 됩니다. 다음을 대신해서 사용합니다.
async function slowEcho(val) {
await wait(1000);
return val;
}
폴리필을 포함하고 다음과 같이 작성합니다.
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
생성기 (function*
)를 createAsyncFunction
에 전달하고 await
대신 yield
를 사용해야 합니다. 그 외에는 동일하게 작동합니다.
해결 방법: 재생기
이전 브라우저를 타겟팅하는 경우 Babel은 생성자를 트랜스파일할 수도 있으므로 IE8까지 비동기 함수를 사용할 수 있습니다. 이렇게 하려면 Babel의 es2017 사전 설정과 es2015 사전 설정이 필요합니다.
출력은 그리 보기 좋지 않으므로 코드 확장에 주의하세요.
모든 것을 비동기화하세요.
비동기 함수가 모든 브라우저에 제공되면 모든 약속 반환 함수에서 사용합니다. 이렇게 하면 코드가 더 깔끔해질 뿐만 아니라 함수가 항상 약속을 반환합니다.
2014년에 비동기 함수에 대해 매우 기뻤는데, 브라우저에 실제로 구현된 것을 보고 기쁩니다. 와!