프로미스로 정상 회복하기
먼저 Promise로 파일에 내용을 추가하고 결과를 다시 읽어오는 예를 살펴보겠습니다.
function appendReadPromise(path: string, data: string): Promise<string>{
return appendReadPromise(path, data)
.then(()=>readPromise(path))
.catch(error=>console.error(error))
}
이 코드는 원하는 일을 완수하는 데 필요한 비동기 작업들을 직관적인 체인(chain) 하나로 엮은 결과, 콜백 피라미드는 전혀 등장하지 않는다는 점에 주목합니다.
한 작업이 성공하면 다른 작업을 실행하며, 그중 하나가 실패하면 catch 절로 직행합니다.
같은 기능을 콜백으로 구현하려면 아래처럼 해야 합니다.
import { appendFile, readFile } from "fs";
function appendRead(
path: string,
data: string,
callback: (error: Error | null, result: string | null) => void
){
appendFile(path, data, error => {
if(error){
return callback(error, null)
}
readFile(path, (error, result) => {
if(error){
return callback(error, null)
}
callback(null, result)
})
})
}
이제부터 이 기능을 제공하는 Promise API를 설계할 것입니다.
가볍게 시작해 보겠습니다.
class Promise {
}
이어서 new Promise는 실행자(executor)라고 부르는 함수를 인수로 받으며, Promise 구현에서 resolve 함수와 reject 함수를 인수로 건네 이 함수를 호출할 것입니다.
type Executor = (
resolve: Function,
reject: Function
) => void
class Promise {
constructor(f: Executor){}
}
resolve와 reject는 어떻게 동작할까요?
다음 코드를 보면서 fs.readFile 같은 콜백 기반의 NodeJS API를 Promise 기반의 API에서 어떻게 수동으로 감쌀 수 있는지 생각해 봅시다.
import { readFile } from "fs";
readFile(path, (error, result) => {
// 블라 블라
})
Promise 구현에서 이 API를 감싸면 다음과 같은 모습이 됩니다.
import { readFile } from "fs"
function readFilePromise(path: string): Promise<string> {
return new Promise((resolve, reject) => {
readFile(path, (error, result)=>{
if(error){
reject(error)
} else {
resolve(result)
}
})
})
}
resolve의 매개변수 타입은 어떤 API를 사용하는지에 따라 달라지며, reject의 매개변수 타입은 항상 Error 유형이 됩니다.
구현으로 돌아와서 안전하지 않았던 Function 타입을 더 구체적인 타입으로 교체해 개선해 보겠습니다.
type Executor<T, E extends Error> = (
resolve: (result: T) => void,
reject: (error: E) => void
) => void
Promise만 보고도 Promise가 어떤 타입으로 해석(resolve)될지를 알고자 하므로 Promise를 제네릭으로 만들고 그 생성자에서 자신의 타입 매개변수들을 Executor 타입에 전달할 것입니다.
type Executor<T, E extends Error> = (
resolve: (result: T) => void,
reject: (error: E) => void
) => void
class Promise<T, E extends Error> {
constructor(f: Executor<T, E>){}
}
Promise의 생성자 API를 정의했고 어떤 타입을 다룰 것인지도 이해했습니다.
이제 API 연쇄에 관해 생각해 보겠습니다.
Promise를 통해 연이어 실행하면서 결과를 전달하고 예외를 잡게끔 하고 싶은 연산자들은 무엇인가요?
맨 위에 등장한 코드의 then과 catch가 이 연산에 해당합니다.
Promise 타입에 이들을 추가하겠습니다.
type Executor<T, E extends Error> = (
resolve: (result: T) => void,
reject: (error: E) => void
) => void
class Promise<T, E extends Error> {
constructor(f: Executor<T, E>){}
then<U, F extends Error>(g: (result: T) => Promise<U, F>): Promise<U, F>
catch<U, F extends Error>(g: (error: E) => Promise<U, F>): Promise<U, F>
}
이러면 이 then과 catch를 이용해 Promise 여러 개를 연쇄적으로 호출할 수 있습니다.
then은 성공한 Promise의 결과를 새 Promise로 매핑하며, catch는 reject 시 에러를 새 Promise로 매핑합니다.
다음으로 then을 활용한 모습을 살펴보겠습니다.
let a: () => Promise<string, TypeError> = // ... 블라 블라
let b: (s: string) => Promise<number, never> = // ... 블라 블라
let c: () => Promise<boolean, RangeError> = // ... 블라 블라
a()
.then(b)
.catch(e => c()) // b는 에러가 아니므로 a가 에러일 때 호출됨
.then(result => console.info('됨!', result))
.catch(e => console.error('에러!', e))
타입 b의 두 번째 인수 타입은 never이므로 b는 절대 에러를 던지지 않음을 의미하며 첫 번째 catch 구문은 a가 에러일 때만 호출됩니다.
하지만 Promise를 이용하면 a가 에러를 던질 수 있지만 b는 그렇지 않을 것이라는 사실을 신경 쓸 필요가 없습니다.
a가 성공하면 Promise를 b로 매핑하고, 그렇지 않으면 첫 번째 catch 구문을 실행하면서 Promise를 c로 매핑하기 때문입니다.
기존의 try/catch 구문의 동작을 흉내 낸 것으로 마치 동기식 동작에 적용되는 try/catch를 비동기 동작에 적용하는 것과 같은 효과를 제공합니다.
Promise가 실제 예외를 던지는 상황도 처리해야 합니다.
then과 catch를 구현할 때 코드를 try/catch로 감싸고 catch 구문에서 거절하는 식으로 처리하면 됩니다.
구체적인 의미는 다음과 같습니다.
- 모든 Promise는 거절될 수 있는 위험이 있으며, 정적으로 이를 확인할 수 없습니다.
- Promise가 거부되었다고 항상 Error인 것은 아닙니다. TypeScript는 어쩔 수 없이 JavaScript의 동작을 상속받는데, JavaScript는 throw로 모든 것을 던질 수 있기 때문입니다. 따라서 거부된 결과가 Error의 서브 타입이라고 간주할 수 없습니다.
이를 감안하여, 에러 타입을 지정하지 않아도 되게끔 Promise 타입을 조금 풀어줍니다.
type Executor<T> = (
resolve: (result: T) => void,
reject: (error: unknown) => void
) => void
class Promise<T> {
constructor(f: Executor<T>){}
then<U>(g: (result: T) => Promise<U>): Promise<U>{
// 블라 블라
}
catch<U>(g: (error: unknown) => Promise<U>): Promise<U>{
// 블라 블라
}
}
이렇게 Promise 인터페이스를 완성했습니다:)
'👶 TypeScript' 카테고리의 다른 글
서버에서 타입스크립트 실행하기 (0) | 2023.02.01 |
---|---|
비동기 스트림 - 이벤트 방출기 (0) | 2023.01.30 |
타입 안전 API (0) | 2023.01.29 |
TSX = JSX + TypeScript (0) | 2023.01.28 |
프론트엔드 프레임워크 - 리액트 (0) | 2023.01.27 |