- 콜백 지옥의 문제점
- 실행의 순서는 코드의 순서와 반대
- 콜백이 중첩된 코드는 직관적으로 이해하기 어려움
- 요청들을 병렬로 실행하거나 오류 상황을 해결하기 어려움
- 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>로
- Promise<Response | never>
- 프로미스를 사용하면 타입스크립트의 모든 타입 추론이 제대로 동작
- 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)]); }
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>
- 타입스크립트는 비동기 코드의 개념을 잡는 데 도움이 됨
요약
- 콜백보다는 프로미스를 사용하는게 코드 작성과 타입 추론 면에서 유리
- 가능하면 프로미스를 생성하기보다는 async/await를 사용
- 간결하고 직관적인 코드를 작성할 수 있고 모든 종류의 오류를 제거할 수 있음
- 어떤 함수가 프로미스를 반환한다면 async로 선언하는 것이 좋음
'책 정리 > 이펙티브 타입스크립트' 카테고리의 다른 글
아이템 41 any의 진화를 이해하기 (0) | 2023.07.26 |
---|---|
아이템 40 함수 안으로 타입 단언문 감추기 (0) | 2023.07.26 |
아이템 26 타입 추론에 문맥이 어떻게 사용되는지 이해하기 (0) | 2023.07.26 |
아이템 14 타입 연산과 제너릭 사용으로 반복 줄이기 (0) | 2023.07.26 |
아이템 13 타입과 인터페이스의 차이점 알기 (0) | 2023.07.26 |