![Typescript 이해하기 - Promise 이해하기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwAkPj%2FbtsA2sz1UT5%2FdbWiGJyt7jKh5p1NLtUwnK%2Fimg.png)
2023.11.26 - [백엔드/Typescript] - Typescript 컴파일 설정 - tsconfig.json
2023.11.26 - [백엔드/Typescript] - Typescript 변수 타입
필요 배경 지식
- Typescript의 기본 변수 선언, 함수 선언 등에 대해서 알고 있어야 합니다.
Prerequisite
- Typescript 컴파일 환경 세팅
Promise가 무엇인가
Promise 의 주요 목표는 동기식 스타일 에러처리를 비동기 / 콜백 스타일 코드로 가져오는것이다. (?)
같은 코드에 대해서 동기식 코드는 다음과 같이 순서대로 이해하기가 쉬울것이다.
function loadJSONSync(filename: string) {
return JSON.parse(fs.readFileSync(filename))
}
// good.json 파일이 정상인 경우
console.log(loadJSONSync('good.json'))
// 파일이 존재하지 않는 경우
try {
console.log(loadJSONSync('absent.json'))
} catch (err) {
console.log('absent.json error', err.message)
}
// 파일이 Json 형태가 아닌 경우
try {
console.log(loadJSONSync('invalid.json'))
} catch (err) {
console.log('invalid.json error', err.message)
}
콜백에 기반한 비 동기 함수 작성시에 두가지를 유의해야한다.
1. 콜백을 두번 호출하면 안된다.
2. 에러를 던지면 안된다.
위 코드를 2가지를 고려하지 않고 비동기식으로 처리하면 아래와 같이 작성할 수 있다.
import fs = require('fs')
// A decent initial attempt .... but not correct. We explain the reasons below
function loadJSON(filename: string, cb: (error: Error, data: any) => void) {
fs.readFile(filename, function(err, data) {
if (err) cb(err)
else cb(null, JSON.parse(data))
})
}
그러나 위의 코드는 JSON.parse 에 잘못된 JSON이 전달되면 콜백이 호출 되지 않는다.
cb함수가 실행하려다가 오류가 발생하면서 프로그램이 깨지게 되는데 다음과 같이 try catch로 감싸면 해결이 가능하다.
다만, 아래의 코드도 1번 문항이 콜백을 두번 호출하지 않는다를 지키지 못한다.
import fs = require('fs')
// A better attempt ... but still not correct
function loadJSON(filename: string, cb: (error: Error) => void) {
fs.readFile(filename, function(err, data) {
if (err) {
cb(err)
} else {
try {
cb(null, JSON.parse(data))
} catch (err) {
cb(err)
}
}
})
}
// load invalid json
loadJSON('invalid.json', function(err, data) {
if (err) console.log('bad.json error', err.message)
else console.log(data)
})
조언 : try catch에 callback을 제외한 모든 동기식 코드를 포함하라.
위의 조언에 따라서 아래와 같이 수정이 가능하다.
import fs = require('fs')
function loadJSON(filename: string, cb: (error: Error) => void) {
fs.readFile(filename, function(err, data) {
if (err) return cb(err)
// Contain all your sync code in a try catch
try {
var parsed = JSON.parse(data)
} catch (err) {
return cb(err)
}
// except when you call the callback
return cb(null, parsed)
})
}
최종적으로는 위와 같이 에러를 핸들링 할 수 있다.
하지만, 자바스크립트에서는 Promise를 통해서 비동기 처리를 더 나은 방향으로 할 수 있다.
Promise 생성 주기
하나의 Promise는 PENDING (대기) 혹은 FULFILLED (이행) 또는 REJECTED (실패) 중에 하나에 해당한다.
Promise 생성자는 상태를 결정하기 위해서 resolve와 reject 함수를 Parameter로 받는다.
const promise = new Promise((resolve, reject) => {
// the resolve / reject functions control the fate of the promise
})
Subscribing to the fate of Promise
이건 굳이 해석을 적는것 보다는 그대로 사용하는게 나을것 같아서 제목을 그대로 뒀다.
요약하자면 Promise는 .then() 을 이용해서 resolved 처리하고, catch()를 사용해서 rejected를 subscribe 할 수 있다.
위의 그림에서 이해해보자면
1. resolved()가 호출되면 PENDING => FULFILLED 상태 변환 -> 이에 따라서 .then()이 호출
2. rejected()가 호출되면 PENDING => REJECTED 상태 변환 -> 이에 따라서 .catch()가 호출
resolve의 예시
const promise = new Promise((resolve, reject) => {
resolve(123)
})
promise.then(res => {
console.log('I get called:', res === 123) // 출력 : I get called: true
})
promise.catch(err => {
// promise에서 생성할때 reject를 부르지 않았으니 호출되지 않음.
})
reject의 예시
const promise = new Promise((resolve, reject) => {
reject(new Error('Something awful happened'))
})
promise.then(res => {
// resolve를 부르지 않았으니 호출 되지 않음.
})
promise.catch(err => {
console.log('I get called:', err.message) // 호출 : I get called: 'Something awful happened'
})
두 함수는 아래와 같이 빠르게 생성할수도 있다.
-
Promise resolved 생성 : Promise.resolve(result)
-
Promise rejected 생성 : Promise.reject(error)
Chain-ability of Promise
Promise를 체이닝 할수도 있다.
위에서 정의한 resolve, reject 규칙을 정확히 따르므로 천천히 읽어보면서 이해해보자.
resolve 예시
// resolve 이므로 .then에서 실행된 값들이 위에서부터 순서대로 출력된다.
Promise.resolve(123)
.then(res => {
console.log(res) // 123
return 456
})
.then(res => {
console.log(res) // 456
return Promise.resolve(123) // Promise를 리턴한다.
})
.then(res => {
console.log(res) // 123 : 직전 resolve(123)에 의해서 실행되므로 123이 전달된다.
return 123
})
reject 예시
체인에 대한 에러처리를 catch 하나로 아래와 같이 해결 할 수도 있다.
// reject Promise를 생성
Promise.reject(new Error('something bad happened'))
.then(res => {
console.log(res) // reject이므로 then은 호출되지 않는다.
return 456
})
.then(res => {
console.log(res) // 위의 then이 호출되지 않았으므로 호출되지 않는다.
return 123
})
.then(res => {
console.log(res) // 위의 then이 호출되지 않았으므로 호출되지 않는다.
return 123
})
.catch(err => {
console.log(err.message) // reject로 인해서 호출된다.
})
Catch는 새로운 Promise를 반환한다.
// reject Promise를 생성한다.
Promise.reject(new Error('something bad happened'))
.then(res => {
console.log(res) // reject()로 인한 rejected 상태라서 실행되지 않는다.
return 456
})
.catch(err => {
console.log(err.message) // reject()로 인한 rejected 상태라서 something bad happened 출력
return 123
})
.then(res => {
console.log(res) // 123 : .catch()의 새로운 Promise가 onfulfilled라서 123 출력
})
.then() 에서 에러코드를 던지면 Promise에서 실패한다.
.then() 안에서 에러코드를 던진 경우 .then() 안에 있더라도 rejected 상태로 변한다.
Promise.resolve(123)
.then(res => {
throw new Error('something bad happened') // 에러를 던진다.
return 456
})
.then(res => {
console.log(res) // 에러로 인해 호출되지 않는다.
return Promise.resolve(789)
})
.catch(err => {
console.log(err.message) // 에러로 인해 호출된다 : something bad happened
})
또 다른 예시
Promise.resolve(123)
.then(res => {
throw new Error('something bad happened') // 에러 호출
return 456
})
.catch(err => {
console.log('first catch: ' + err.message) // 에러로 인한 catch 호출 : something bad happened
return 123
})
.then(res => {
console.log(res) // 123 : 위의 .catch()가 새로운 Promise 호출하여 호출됨.
return Promise.resolve(789)
})
.catch(err => {
console.log('second catch: ' + err.message) // 호출 되지 않음.
})
따라서, 위와 같이 Promise를 잘 활용하면 비동기를 사용하는 경우에도 동기와 비슷한 형태의 코드를 작성할 수 있다.
TypeScript 와 Promise
위의 예시들과 같이 타입스크립트의 장점은 체인의 값의 흐름을 쉽게 이해할 수 있다는 점이다.
.then()이나 .catch() 부분에서 어떤것을 리턴하는지만 잘 따라가면 비동기 함수가 어떤걸 값으로 전달하는지 쉽게 이해할 수 있다.
Promise.resolve(123)
.then(res => {
// resolve(123) 실행에 따라 실행되므로 res는 123임을 알 수 있다.
return true
})
.then(res => {
// 위의 .then()에 의해서 실행되므로 res는 boolean 값임을 알 수 있다.
})
function iReturnPromiseAfter1Second(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve('Hello world!'), 1000)
})
}
Promise.resolve(123)
.then(res => {
// res는 123이다.
return iReturnPromiseAfter1Second() // `Promise<string>`를 리턴
})
.then(res => {
// res는 string이다. ("hello world!" 이다.)
console.log(res) // Hello world!
})
이제 Callback style을 Promise 리턴 방식으로 고쳐보자.
함수의 호출을 Promise로 감싸고
1. 에러가 발생하면 reject
2. 에러가 없다면 resolve
로 처리한다.
import fs = require('fs')
function readFileAsync(filename: string): Promise<any> {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
}
NodeJS에서는 아래와 같이 함수 쉽게 작성한다.
/** Sample usage */
import fs from 'fs'
import util from 'util'
const readFile = util.promisify(fs.readFile)
Node 콜백 스타일의 함수를 멤버로 가지고 있다면 bind 해주어야 올바르게 적용된다고 한다. (정확한 의미는 이해하지 못했다 ㅠ )
const dbGet = util.promisify(db.get).bind(db);
다시 예제로 돌아가서
첫번째 loadJSON 예제를 비동기 Promise 버전으로 변경하면 다음과 같이 사용할 수 있다.
function loadJSONAsync(filename: string): Promise<any> {
return readFileAsync(filename) // Use the function we just wrote
.then(function(res) {
return JSON.parse(res)
})
}
아래를 이전의 sync버전과 얼마나 비슷한지 비교해 볼수 있을것이다.
// good json file
loadJSONAsync('good.json')
.then(function(val) {
console.log(val)
})
.catch(function(err) {
console.log('good.json error', err.message) // 파일이 정상인 경우 호출되지 않음.
})
// 파일이 존재하지 않을때
.then(function() {
return loadJSONAsync('absent.json')
})
.then(function(val) {
console.log(val) // 파일이 존재하지 않는 경우 호출되지 않음.
})
.catch(function(err) {
console.log('absent.json error', err.message)
})
// JSON 파일이 포맷이 이상 할 때
.then(function() {
return loadJSONAsync('invalid.json')
})
.then(function(val) {
console.log(val) // 파일 포맷이 이상하면 여기는 호출되지 않음.
})
.catch(function(err) {
console.log('bad.json error', err.message)
})
Parallel control flow
Sequential한 작업에 대해서는 위의 예시에서 알아볼 수 있었다.
그러나, 일련의 비동기 작업들을 실행한 후 이런 작업의 결과로 무언가를 수행하려 할 수도 있을 것이다.
그럴때는 Promise.all 함수를 이용해서 Promise 에 숫자를 인자로 전달해서 실행할수 있다.
Promise를 배열 형태로 실행하고 전달 받는 값도 배열 형태로 전달받는다.
// 딜레이를 주는 비동기 함수
function loadItem(id: number): Promise<{ id: number }> {
return new Promise(resolve => {
console.log('loading item', id)
setTimeout(() => {
// simulate a server delay
resolve({ id: id })
}, 1000)
})
}
// Chained / 순차적 실행
let item1, item2;
loadItem(1)
.then(res => {
item1 = res
return loadItem(2)
})
.then(res => {
item2 = res
console.log('done')
}) // overall time will be around 2s
// Concurrent / Parallel
Promise.all([loadItem(1), loadItem(2)])
.then((res) => {
[item1, item2] = res;
console.log('done');
}); // overall time will be around 1s
때때로, 비동기 함수를 같이 실행하고 먼저 끝나는 값만 가져오고싶을 수 있다.
그럴때는 promise.race 함수를 이용해서 이를 해결 할 수 있다.
var task1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, 'one')
})
var task2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 2000, 'two')
})
Promise.race([task1, task2]).then(function(value) {
console.log(value) // "one"
// 둘다 resolve지만 task1이 먼저 끝난다. 따라서 task1이 출력된다.
})
결론
- Promise에 대해서 천천히 이해해 보았다.
Reference
프로미스 - TypeScript Deep Dive
loadJSONSync는 3가지 동작 유효한 리턴값 파일, 시스템 에러, JSON.parse 에러가 있습니다. 우리는 에러를 try/catch를 사용해서 쉽게 조작할 수 있습니다. 이것은 동기식 프로그래밍 언어에 존재합니다.
radlohead.gitbook.io
'백엔드 > NodeJS | Typescript' 카테고리의 다른 글
Typescript 프로젝트의 naming convention (0) | 2023.12.07 |
---|---|
Typescript 이해하기 - Async/Await 이해하기 (0) | 2023.11.28 |
Typescript 이해하기 - 제너레이터 이해하기 (0) | 2023.11.28 |
Typescript 변수 타입 (0) | 2023.11.26 |
Typescript 컴파일 설정 - tsconfig.json (0) | 2023.11.26 |
개발 및 IT 관련 포스팅을 작성 하는 블로그입니다.
IT 기술 및 개인 개발에 대한 내용을 작성하는 블로그입니다. 많은 분들과 소통하며 의견을 나누고 싶습니다.