책 정리/이펙티브 타입스크립트

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

Rulu_ 2023. 7. 26. 14:33
  • 콜백 지옥의 문제점
    • 실행의 순서는 코드의 순서와 반대
    • 콜백이 중첩된 코드는 직관적으로 이해하기 어려움
    • 요청들을 병렬로 실행하거나 오류 상황을 해결하기 어려움
  • fetchURL(url1, function(response1) { fetchURL(url2, function(response2) { fetchURL(url3, function(response3) { // ... console.log(1); }); console.log(2); }); console.log(3); }); console.log(4); //출력: 4 3 2 1
  • Promise
    • 콜백 지옥을 해결하기 위해 도입 (미래에 가능해질 어떤 것)
    • 실행의 순서는 코드의 순서와 동일
    • 오류 처리, Promise.all과 같은 고급 기법 사용에 용이
  • const page1Promise = fetch(url1); page1Promise.then(response1 => { return fetch(url2); }).then(response2 => { return fetch(url3); }).then(response3 => { // ... }).catch(error => { // ... });
  • async, await
    • 각 프로미스가 처리(resolve)될 때까지 기다림
    • async 함수 내에서 await 중인 프로미스가 거절(reject)되면 예외를 던짐
      • 일반적인 try/catch 사용 가능
    • 이전 버전에서 작동할 때 타입스크립트 컴파일러는 async와 awaut가 동작하도록 변환
    • 타입스크립트는 런타임과 관계 없이 async/await 사용 가능
  • async/await 사용해야 하는 이유
    • 코드를 작성하기 쉬움
    • 타입을 추론하기 쉬움
    • Promise.all 사용 가능
    • async function fetchPages() { const [response1, response2, response3] = await Promise.all([ fetch(url1), fetch(url2), fetch(url3) ]); // ... }

async와 타입 추론

  • await와 구조 분해 할당
    • 구조 분해 할당
      • 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식
      • […array]
    function fetchPagesCB() {
      let numDone = 0;
      const responses: string[] = [];
      const done = () => {
        const [response1, response2, response3] = responses; // 구조 분해 할당
        // ...
      };
     // ...
    }
    
    • 세 가지 response 변수의 각 타입을 Response로 추론
  • Promise.race 사용 시
    • 타입 구문이 없어도 fetchWithTimeout 의 반환 타입 Promise<Response>로 추론
    • 추론이 동작하는 이유
      • Promise.race의 반환 타입은 입력 타입들의 유니온
        • Promise<Response | never>
          • never는 일반적으로 함수의 리턴 타입으로 사용 함수의 리턴 타입으로 never가 사용될 경우, 항상 오류를 출력하거나 리턴 값을 절대로 내보내지 않음을 의미
        • never(공집합)와의 유니온은 아무런 효과가 없음 → 결과가 Promise<Response>로
      • 프로미스를 사용하면 타입스크립트의 모든 타입 추론이 제대로 동작
  • 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)]); }

async/await 사용해야하는 이유

  • 간결하고 직관적인 코드
  • async 함수는 항상 프로미스를 반환하도록 강제
    • async 화살표 함수
    const getNumber = async() => 42;  // Type is () => Promise<number>
    
    • 직접 프로미스 생성
    const getNumber = () => Promise.resolve(42);  // Type is () => Promise<number>
    
    • 함수는 항상 동기 또는 비동기로 실행되어야 하며 절대 혼용해서는 안됨.
      • 즉시 사용 가능한 값에도 프로미스를 반환하도록 하면 비동기 함수로 통일이 강제
      • 비동기 함수로 통일이 안된 경우(비추천)
        • 캐시된 경우 콜백 함수가 동기로 호출되기 때문에 fetchWithCache() 사용하기 어려움
        let requestStatus: 'loading' | 'success' | 'error';
        function getUser(userId: string) {
          fetchWithCache(`/user/${userId}`, profile => {
            requestStatus = 'success';
          });
          requestStatus = 'loading';
        }
        
        • 캐시되어 있지 않다면 success로 변경되고, 캐시되어 있다면 success되고 나서 바로 loading으로 변경됨
      • const _cache: {[url: string]: string} = {}; function fetchWithCache(url: string, callback: (text: string) => void) { if (url in _cache) { callback(_cache[url]); // 얘는 동기 } else { fetchURL(url, text => { _cache[url] = text; callback(text); }); // 얘는 비동기 } }
      • async를 사용해 통일 한 경우(추천)
        • 일관적인 동작을 강제
        • 캐시된 경우와 안되어있던 경우 모두 success로 동작함
      • const _cache: {[url: string]: string} = {}; async function fetchWithCache(url: string) { if (url in _cache) { return _cache[url]; } const response = await fetch(url); const text = await response.text(); _cache[url] = text; return text; } let requestStatus: 'loading' | 'success' | 'error'; async function getUser(userId: string) { requestStatus = 'loading'; const profile = await fetchWithCache(`/user/${userId}`); requestStatus = 'success'; }
      • 콜백이나 프로미스를 사용하면 실수로 반(half)동기 코드를 작성할 수 있지만, async를 사용하면 항상 비동기 코드를 작성한 셈
  • async함수에서 프로미스를 반환하면 또 다른 프로미스로 래핑되지 않음
    • 언제나 반환타입은 Promise<T>
  • 타입스크립트는 비동기 코드의 개념을 잡는 데 도움이 됨

요약

  1. 콜백보다는 프로미스를 사용하는게 코드 작성과 타입 추론 면에서 유리
  2. 가능하면 프로미스를 생성하기보다는 async/await를 사용
    1. 간결하고 직관적인 코드를 작성할 수 있고 모든 종류의 오류를 제거할 수 있음
  3. 어떤 함수가 프로미스를 반환한다면 async로 선언하는 것이 좋음