[Effective TypeScript] 타입 추론(4)

이예슬·2022년 11월 29일
0

Effective TypeScript

목록 보기
10/15

Item25. 비동기 코드에는 콜백 대신 async 함수 사용하기

자바스크립트에서는 비동기 동작을 구현하기 위해 콜백, 프로미스, async/await 세 가지 방법을 사용할 수 있다. 이 중 가장 역사가 오래된 방법은 콜백이지만 콜백이 중첩된 코드는 직관적으로 이해하기 어려우며 요청을 병렬로 실행하거나 오류 상황을 빠져나오기 어렵다. ES2015는 이러한 콜백 지옥을 극복하기 위해 프로미스 개념을 도입했으며 ES2017에서는 async/await 키워드가 도입되었다.

ES5 또는 더 이전 버전을 대상으로 할 때 타입스크립트 컴파일러는 async와 await가 동작하도록 정교한 변환을 수행한다. 즉 타입스크립트는 런타임에 관계없이 async/await를 사용할 수 있다.

프로미스와 async/await은 콜백보다 코드를 작성하기 쉬우면서 타입을 추론하기도 쉽다.

//Promise.all 
async function fetchPages(){
  const [response1, response2, response3] = await Promise.all([
    fetch(url1), fetch(url2), fetch(url3)
  ])
}

위 코드처럼 병렬로 페이지를 로드하고 싶다면 Promise.all 을 사용해서 프로미스를 조합하면 된다.

//Promise.race 
async function doSomethingWithTimeout(something: string, ms: number){
  return Promise.race([fetchPages(), timeout(ms)])
}

입력된 프로미스들 중 첫 번째가 처리될 때 완료되는 Promise.race 도 타입 추론과 잘 맞는다. Promise.race 를 사용하여 프로미스에 타임아웃을 추가하는 방법은 흔히 사용하는 패턴이다. 위 코드에서는 타입 구문이 없어도 doSomethingWithTimeout 의 반환타입은 Promise<Response> 로 추론된다. 즉 프로미스를 사용하면 타입스크립트의 모든 타입 추론이 제대로 동작한다.

프로미스를 직접 생성해야 할 때 선택의 여지가 있다면 프로미스보다는 async/await를 사용해야 한다. 프로미스보다는 async/await를 선택해야 하는 이유는 다음과 같다.

  • 일반적으로 더 간결하고 직관적인 코드가 된다.
  • async 함수는 항상 프로미스를 반환하도록 강제된다.
async function getNumber(){
  return 42;
}
const getNumberArrow = async() => 42; 
const getNumberPromise = () => Promise.resolve(42)

function test (){
  return Promise.resolve(Promise.resolve(42))
} //반환 타입은 Promise<Promise<number>>가 아닌 Promise<number> 

위 네 함수의 반환값의 타입은 모두 Promise로 동일하다. 즉시 사용 가능한 값에도 프로미스를 반환하는 것이 이상하게 보일 수 있지만 실제로는 비동기 함수로 통일하도록 강제하는 데 도움이 된다. 함수는 항상 동기 또는 비동기로 실행되어야 하며 절대 혼용해서는 안된다.

Item26. 타입 추론에 문맥이 어떻게 사용되는지 이해하기

타입스크립트는 타입을 추론할 때 단순히 값만 고려하는 것이 아닌 값이 존재하는 곳의 문맥까지도 살핀다.

type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(lang : Language){
  console.log('laguage is', lang)
}

let lang = 'JavaScript'

setLanguage('JavaScript')
setLanguage(lang) //❗️string type은 'Language' 형식의 매개변수에 할당될 수 없다. 

위와 같은 에러는 타입스크립트가 할당 시점에 타입을 추론하기 때문에 발생했다. lang은 변수 할당 시점에 string으로 타입이 추론되었으므로 setLanguage에는 할당할 수 없다는 에러가 발생한 것이다.

위와 같은 문제를 해결하기 위해 타입을 명시적으로 선언하거나 lang을 상수로 만들 수 있다. 하지만 두 과정 모두 사용되는 문맥과 값을 분리하는 방법이다. 이러한 방법은 추후 근본적인 문제를 발생시킬 수 있다.

튜플 사용 시 주의점

문자열 리터럴 타입과 마찬가지로 튜플 타입에서도 문제가 발생한다.

function doSomething(something : [number, number]){
  console.log(something)
}

const foo = [1, 2]

doSomething(foo)//❗️number[] 형식의 인수는 '[number, number]' 형식의 매개변수에 할당될 수 없다. 
doSomething([1, 2])

타입스크립트는 foo의 타입을 number[] 로 추론하기 때문에 튜플 타입에 할당할 수 없다. 이러한 오류를 해결하기 위해 상수 문맥을 사용할 수 있다. const는 단지 값이 가리키는 참조가 변하지 않는 얕은 상수인 반면, as const는 그 값이 내부까지 상수라는 사실을 타입스크립트에게 알려준다. 하지만 as const로 선언될 경우 readonly로 추론되므로 함수를 사용하는 곳에서도 readonly 구문을 추가해야 한다.

하지만 위와 같이 문맥과 값을 분리할 경우 타입 정의에 실수가 있다면 오류는 타입 정의가 아니라 호출되는 곳에서 발생한다.

Item27. 함수형 기법과 라이브러리로 타입 흐름 유지하기

타입 흐름을 개선하고 가독성을 높이고 명시적인 타입 구문의 필요성을 줄이기 위해 직접 구현하기 보다는 내장된 함수형 기법과 로대시 같은 유틸리티 라이브러리를 사용하는 것이 좋다. 라이브러리들의 일부 기능은 순수 자바스크립트로 구현되어 있다. 이러한 기법들을 타입스크립트와 사용하게 되면 타입 정보가 그대로 유지되면서 타입 흐름이 계속 전달되도록 하기 때문이다.

const rows = rawRows.slice(1).map(rowStr => {
	const row = {} 
	rowStr.split(',').forEach((val, j) => {
		row[headers[j] = val;
	});
	return row; 
});

import _ from 'loadash';
const rows = rawRows.slice(1)
	.map(rowStr => _.zipObject(headers, rowStr.split(',')))

위 코드는 csv 데이터를 파싱한다고 생각해 볼 때 순수 자바스크립트에서 절차형 프로그래밍 형태로 구현한 코드와 lodash의 zipObject함수를 이용해서 구현한 코드이다. 키와 값 배열로 취합해서 객체로 만들어주는 lodash의 zipObject 함수를 사용하면 코드를 더욱 짧게 만들 수 있다.

그런데 자바스크립트에서는 프로젝트에 서드파티 라이브러리 종속성을 추가할 때 신중해야 한다. 하지만 같은 코드를 타입스크립트로 작성하면 서드파티 라이브러리를 사용하는 것이 무조건 유리하다. 타입 정보를 참고하며 작업하므로 서드파티 라이브러리 기반으로 바꾸는 데 시간이 훨씬 단축되기 때문이다.


<이펙티브 타입스크립트> Dan Vanderkam, 프로그래밍 인사이트 (2021)

profile
꾸준히 열심히!

0개의 댓글