아이템 25. 비동기 코드에는 콜백 대신 async 함수 사용하기 #
ES2015, 프로미스(promis) 개념이 도입되었다.
- 프로미스는 미래에 가능해질 어떤 것을 나타낸다. (future)
ES2017, async/await 키워드가 도입되었다.
await
: 각각의 프로미스가 처리(resolve)될 때까지 함수의 실행을 멈춘다.async
: 함수 내에서 await 중인 프로미스가 거절(reject)되면 예외를 던진다.- 이를 통해 일반적인
try/catch
구문을 사용할 수 있다.
- 이를 통해 일반적인
아래 예시를 살펴보자. 콜백 지옥, 프로미스, async/await 를 사용하여 코드를 개선한 예시이다.
/** 예시 : 콜백 */
fetchURL(url1, function(response1) {
fetchURL(url2, function(response2) {
fetchURL(url3, function(response3) {
//...
console.log(1);
})
console.log(2);
})
console.log(3);
})
console.log(4);
/** 예시 : 프로미스 */
const page1Promise = fetch(url1);
page1Promise.then(response1 => {
return fetch(url2);
}).then(response2 => {
return fetch(url3);
}).then(response3 => {
// ...
}).catch(error => {
// ...
})
/** 예시 : async/await */
async function fetchPages() {
const response1 = await fetch(url1);
const response2 = await fetch(url2);
const response3 = await fetch(url3);
}
" ES 또는 더 이전 버전을 대상으로 할 때, 타입스크립트 컴파일러는 async와 await가 동작하도록 정교한 변환을 수행한다. 다시 말해, 타입스크립트는 런타임에 관계없이 async/await를 사용할 수 있다."
콜백보다 프로미스, async/await 를 사용해야 하는 이유 #
- 코드를 작성하기 쉽다.
- 타입을 추론하기 쉽다.
아래 예시는 (프로미스)병렬 처리 예시이다.
async function fetchPages() {
const [response1, response2, response3] = await Promise.all([
fetch(url1), fetch(url2), fetch(url3)
]);
// ...
}
이런 경우 ‘await
’, ‘구조 분해 할당’과 궁합이 잘 어울린다. (<- ?)
타입스크립트는 3가지 response 변수 각각의 타입을 Response 로 추론한다.
콜백 스타일로 코드를 작성하려면 더 많은 코드와 타입 구문이 필요하다.
function fetchPagesCB() {
let numDone = 0;
const responses: string[] = [];
const done = () => {
const [response1, response2, response3] = responses; // 구조 분해 할당
// ...
};
const urls = [url1, url2, url3];
urls.forEach((url, i) => {
fetchURL(url, r => {
responses[i] = url;
numDone++;
if(numDone === urls.length) done();
})
})
}
이 코드에 ‘오류 처리’를 포함하거나 Promise.all
같은 일반적인 코드로 확장하는 것은 쉽지 않다.
Promise.race
#
한편 입력된 프로미스들 중 첫 번째가 처리될 때 완료되는 Promise.race
도 타입 추론과 잘 맞는다.
Promise.race
를 사용하여 프로미스에 ‘타임아웃’을 추가하는 방법은 흔하게 사용되는 패턴이다.
function timeout(millis: number): Promise<never> {
return new Promise((resolve, reject) => {
setTimeout(() => reject('timeout'), millis);
})
}
async function fetchWithTimeout(url: string, ms: number) {
return Promise.race([fetch(url), timeout(ms)]); // 타입 : Promise<Response>
}
fetchWithTiemout
의 반환 타입은 Promise<Response>
이다.
" Promise.race 의 반환 타입은 입력 타입들의 유니온이고, 이번 경우는
Promise<Response | never>
가 된다. 그러나 never(공집합)와의 유니온은 아무런 효과가 없으므로, 결과(타입)가Promise<Response>
로 간단해진다. “” 프로미스를 사용하면 타입스크립트의 모든 타입 추론이 제대로 동작한다. “
(선택의 여지가 있다면) 프로미스를 직접 생성하기 보다는 async/await
를 사용해야한다. (권장한다.)
#
- 일반적으로 더 간결하고 직관적인 코드가 된다.
- async 함수는 항상 프로미스를 반환하도록 강제한다.
- 콜백, 프로미스(직접 사용)를 사용하면 반(half)동기 코드를 작성할 수 있다.
- async 사용하면 비동기를 보장한다.
- async 함수에서 프로미스 반환 시 또 다른 프로미스로 래핑되지 않는다.
Promise<Promise<T>>
가 아니라,Promise<T>
가 된다.
async function getNumber() { // 타입 : Promise<number>
return 42;
}
위 처럼 즉시 사용 가능한 값(42)에도 프로미스를 반환하는 것이 이상하게 보일 수 있지만, 실제로는 비동기 함수로 통일하도록 강제하는 데 도움이 된다.
함수는 항상 동기 또는 비동기로 실행되어야 하며 절대 혼용해서는 안된다.
책에서 이에 대한 예시가 있다. 동기, 비동기를 혼용해서 사용하면 코드의 동작(결과)가 달라질 수 있다. (p140 ~ p141)
요악 & 정리 #
- 콜백 «« 프로미스 «« async/await
- 코드 작성, 타입 추론 면에서 유리하다.
- 어떤 함수가 프로미스를 반환한다면 async로 선언하는게 좋다.