Skip to content

Latest commit

 

History

History
278 lines (214 loc) · 9.17 KB

File metadata and controls

278 lines (214 loc) · 9.17 KB

Item 25 ~ Item 27

Item 25: Use async Functions Instead of Callbacks for Asynchronous Code

JS의 전통적인 비동기 처리방식은 콜백이었는데, 코드 순서와 실행 순서가 반대가 되기 때문에 읽기 힘들었다.

fetchURL(url1, function(response1) { 
	fetchURL(url2, function(response2) {
		fetchURL(url3, function(response3) { 
			// ...
      console.log(1);
	  });
    console.log(2);
  });
  console.log(3);
});
console.log(4);

// Logs:
// 4
// 3
// 2
// 1

ES2015에서는 Promise, ES2017에서는 async await 의 도입으로 비동기 코드의 작성이 편해졌고 Typescript에서도 async/await 을 그대로 사용하면 된다.

요청을 병렬로 처리하려면 Promise.all 을 사용한다. 각각의 response 타입도 추론해준다.

async function fetchPages() {
	const [response1, response2, response3] = await Promise.all([
		fetch(url1), fetch(url2), fetch(url3)
	]);
	// ...
}
  • 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)]); 
    }

    위 코드에서 fetchWithTimeout 의 리턴타입은 Promise<Response> 으로 추론되는데, Promise<Response|never> 에서 never 와의 유니온은 항상 사라지기 때문

가끔은 Promise 를 직접 써야 할 경우도 있지만, 어지간하면 async/await 을 쓰도록 하자. 코드도 더 명확해지고, async 함수는 항상 Promise를 리턴한다는 점이 좋다.

// function getNumber(): Promise<number>
async function getNumber() { 
	return 42;
}
const getNumber = async () => 42; // Type is () => Promise<number>
const getNumber = () => Promise.resolve(42); // Type is () => Promise<number>

바로 가져올 수 있는 값을 왜 Promise로 리턴하는지 궁금할 수 있는데, 다음 규칙을 지키기 쉽게 해준다.

어떤 함수를 만들 때 항상 동기로 작동하게 하거나 항상 비동기로 작동하게 해라. 둘을 섞어서는 안된다.

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';
}

fetchWithCache 는 캐시된 값이 있어도 없어도 항상 Promise를 리턴하기 때문에, 코드가 일관적으로 우리가 의도한 순서대로 동작한다.

(비슷한 경우 https://github.com/lunit-io/insight-front/blob/c300745bae8b4bb830b71df4a3e3224b1614baac/projects/insight-mmg-report/src/service/pdfReport.ts#L58)

async 함수 내에서 Promise를 리턴해도 Promise가 중첩되지 않는다는 점은 기억해두자.

Item 26: Understand How Context Is Used in Type Inference

TypeScript는 변수가 처음 등장할때 타입을 추론한다. 그래서 변수가 사용되는 곳에서 더 좁은 타입을 요구한다면 에러가 나게 된다.

type Language = 'JavaScript' | 'TypeScript' | 'Python'; 
function setLanguage(language: Language) { /* ... */ }

setLanguage('JavaScript'); // OK

let language = 'JavaScript'; 
setLanguage(language);
         // ~~~~~~~~ Argument of type 'string' is not assignable
         //          to parameter of type 'Language'

let language: Language = 'JavaScript'; 
setLanguage(language); // OK

const language = 'JavaScript'; 
setLanguage(language); // OK

좀 더 근본적으로 보자면 값이 사용될 맥락을 알려주지 않았기 때문에 문제가 된 것

Tuple Types

// Parameter is a (latitude, longitude) pair. 
function panTo(where: [number, number]) { /* ... */ }
panTo([10, 20]); // OK 

const loc = [10, 20];
panTo(loc);
//    ~~~ Argument of type 'number[]' is not assignable to
//        parameter of type '[number, number]'

위에서도 loc가 처음 등장한 라인만 보면 Typescript로서는 number[] 타입으로 추론할수밖에 없다.

타입 선언으로 해결할수도 있겠지만, loc 변수의 값이 고정된 상수라는 맥락을 알려줘서 해결해보자.

const loc = [10, 20] as const; 
panTo(loc);
   // ~~~ Type 'readonly [10, 20]' is 'readonly'
   //     and cannot be assigned to the mutable type '[number, number]'

이번에는 추론이 너무 좁게 되어서 에러가 났다.

function panTo(where: readonly [number, number]) { /* ... */ } 
const loc = [10, 20] as const;
panTo(loc); // OK

함수의 타입 시그너처 자체를 고쳐서 에러를 해결

Objects

위에서처럼 선언을 쓰거나 as const 단언으로 해결하면 됨

Callbacks

function callWithRandomNumbers(fn: (n1: number, n2: number) => void) { 
	fn(Math.random(), Math.random());
}
const fn = (a, b) => {
				 // ~ Parameter 'a' implicitly has an 'any' type
				 //    ~ Parameter 'b' implicitly has an 'any' type
	console.log(a + b);
}
callWithRandomNumbers(fn);

const fn = (a: number, b: number) => { 
	console.log(a + b);
}
callWithRandomNumbers(fn);

위와 같이 인자에 타입을 써줘도 되고, Item 12에서처럼 콜백 함수 전체의 선언을 만들어줘도 된다.

Item 27: Use Functional Constructs and Libraries to Help Types Flow

JS에는 다른 언어에서 많이들 제공하는 표준 라이브러리가 없다. 그래서 여러 라이브러리들이 아쉬운 부분을 채우기 위해 등장했고, map, flatMap, filter, reduce와 같은 기능들은 JS로 편입되기도 했다. 이런 함수형 기법에 TypeScript를 끼얹으면 직접 짠 반복문보다 훨씬 편리해진다. 내가 직접 타입을 관리해야 하는 반복문과 달리 함수형 기법에서 제공하는 타입 정의를 활용할 수 있기 때문.

const csvData = "...";
const rawRows = csvData.split('\n'); 
const headers = rawRows[0].split(',');
const rows = rawRows.slice(1)
	.map(rowStr => rowStr.split(',').reduce(
		(row, val, i) => (row[headers[i]] = val, row),
		{}));

위 코드도 깔끔하긴 하지만, Lodash의 zipObject 를 사용하면 더 쉬워진다.

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

하지만 라이브러리를 추가하면 번들 사이즈가 늘어나는 등의 부담이 있다. 이 때 TypeScript를 함께 사용하면 라이브러리 사용의 이점이 더 커진다.

TypeScript로 짠다면 아래 코드는 타입에러가 발생한다.

const rowsB = rawRows.slice(1) 
	.map(rowStr => rowStr.split(',').reduce(
		(row, val, i) => (row[headers[i]] = val, row),
									 // ~~~~~~~~~~~~~~~ No index signature with a parameter of
                   //                 type 'string' was found on type '{}'
		{}));

Lodash 버전은 그대로 사용 가능하다. {[key: string]: string} 또는 Record<string, string> 의 별칭인 Dictionary<string> 타입을 사용했기 때문.

const rows = rawRows.slice(1)
	.map(rowStr => _.zipObject(headers, rowStr.split(','))); 
	// Type is _.Dictionary<string>[]

예시는 NBA 팀들의 선수 로스터 코드

interface BasketballPlayer { 
	name: string;
	team: string;
	salary: number;
}
declare const rosters: {[team: string]: BasketballPlayer[]};

선수들로 목록을 만든다면

const allPlayers = Object.values(rosters).flat();
// OK, type is BasketballPlayer[]

각 팀에서 가장 연봉이 높은 선수들로 목록을 만든다면

const teamToPlayers: {[team: string]: BasketballPlayer[]} = {}; 
for (const player of allPlayers) {
	const {team} = player;
	teamToPlayers[team] = teamToPlayers[team] || []; 
	teamToPlayers[team].push(player);
}
for (const players of Object.values(teamToPlayers)) { 
	players.sort((a, b) => b.salary - a.salary);
}
const bestPaid = Object.values(teamToPlayers).map(players => players[0]); 
bestPaid.sort((playerA, playerB) => playerB.salary - playerA.salary); 
console.log(bestPaid);

Lodash로 같은 코드를 짜면

const bestPaid = _(allPlayers)
	.groupBy(player => player.team)
	.mapValues(players => _.maxBy(players, p => p.salary)!) 
	.values()
	.sortBy(p => -p.salary)
	.value() // Type is BasketballPlayer[]

코드 길이도 더 짧고, 체이닝을 통해 _.c(_.b(_.a(v))) 대신 _(v).a().b().c().value() 처럼 자연스러운 순서로 코드를 작성할 수 있다.

TypeScript와 함께라면 Lodash의 단축어들에서 매우 정확한 타입 추론을 사용할 수 있는 점도 좋다.

const mix = _.map(allPlayers, Math.random() < 0.5 ? 'name' : 'salary');
  // Type is (string | number)[]

이게 다 mutation을 피하고 매번 새 값을 새 타입과 함께 리턴하는 구현 방식 덕분.