아이템 25. 비동기 코드에는 콜백 대신 Async 함수 사용하기

아이템 25. 비동기 코드에는 콜백 대신 Async 함수 사용하기

아이템 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 를 사용해야 하는 이유 #

  1. 코드를 작성하기 쉽다.
  2. 타입을 추론하기 쉽다.

아래 예시는 (프로미스)병렬 처리 예시이다.

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로 선언하는게 좋다.