비동기 함수를 사용하면 마치 동기 함수인 것처럼 프로미스 기반 코드를 작성할 수 있습니다.
비동기 함수는 Chrome, Edge, Firefox, Safari에서 기본적으로 사용 설정되어 있으며 정말 놀라운 기능입니다. 이 함수를 사용하면 기본 스레드를 차단하지 않고 동기식인 것처럼 프로미스 기반 코드를 작성할 수 있습니다. 비동기 코드를 덜 '똑똑'하고 더 읽기 쉽게 만듭니다.
비동기 함수는 다음과 같이 작동합니다.
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!';
}
위 작업은 완료하는 데 500밀리초가 걸립니다. 두 대기가 동시에 발생하기 때문입니다. 실제 예를 살펴보겠습니다.
예: 가져오기(fetch)를 순서대로 출력
일련의 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의 폴리필을 가져와 직접 사용하면 됩니다. 다음을 대신해서 사용합니다.
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년에 비동기 함수에 대해 정말 큰 기대를 했습니다. 그리고 실제로 브라우저에서 비동기 함수를 사용하는 모습을 보니 기뻐요. 와!