프로미스는 비동기로 실행되는 작업의 완료 상태와 리턴값, 완료시 실행될 콜백 함수를 저장하는 객체입니다.
비동기 작업이 완료되기 전에 미리 생성할 수 있어 비동기 함수가 동기 함수처럼 값을 리턴할 수 있게 해줍니다.
프로미스의 상태(State)는 다음의 세 가지 값을 가질 수 있습니다 :
| 상태 | 설명 |
|---|---|
| pending(대기) | 이행하지도, 거부하지도 않은 초기 상태 |
| fulfilled(이행) | 연산이 성공적으로 완료됨 |
| rejected | 연산이 실패함 |
상태와 비슷하지만 다른 개념인 Fate도 있습니다 :
| Fate | 설명 |
|---|---|
| resolved | 이행하거나 거부한다고 해도 아무런 영향을 미치지 않는 상태. 즉 프로미스가 이미 fullfilled / rejected 상태이거나 다른 프로미스에 'locked in' 되어있음 |
| unresolved | 상태가 변화할 여지가 남아있음 i.e. 이행하거나 거부하려는 시도가 프로미스에 영향을 미칠 수 있음 |
const promise = new Promise((resolve, reject) => {
if(/* 비동기 처리 성공 */) {
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
resolve, reject는 자바스크립트에서 제공하는 함수로 사용자가 정의할 필요는 없습니다. resolve / reject 함수의 인자로 실제 실행하고 싶은 로직을 넣으면 됩니다.프로미스를 리턴하는 함수를 여러 개를 호출해야 하는데, 각 함수의 호출 순서가 중요하다면 - 즉 a 함수가 b 함수에 의존한다면, b 함수의 처리 이후에 a 함수가 처리되어야 할 수 있습니다. 이처럼 비동기 작업이지만 동기적으로 처리되어야 하는 상황이 빈번히 발생합니다.
이럴 때 async / await를 사용하면 프로미스를 동기 처리처럼 사용할 수 있습니다. 프로미스를 리턴하는 함수 앞에 await 키워드를 붙이면 됩니다.
await this.callDatabase();
await 키워드는 받은 프로미스가 완료될 떄까지 기다렸다가 resolve한 처리 결과를 반환합니다.
await 키워드는 async 키워드가 붙은 함수에서만 사용할 수 있습니다.
Promise.all은 반대로 프로미스를 병렬로 실행하고 싶을 때 사용합니다. 사실 비동기 작업은 그대로 두어도 병렬로 실행되지만, Promise.all을 사용하면 여러 프로미스를 하나로 묶어 모든 프로미스가 완료될 때까지 기다린 뒤 그 리턴값을 사용할 수 있습니다.
Promise.all은 프로미스의 정적 메소드로 프로미스의 배열(이터러블)을 인자로 받아 하나씩 순회합니다. 만약 하나라도 실패하면 실행을 중단하고 에러를 리턴합니다. 모두 성공시 각 프로미스의 리턴값을 담은 하나의 프로미스를 리턴합니다.
이와 비슷하게 Promise.allsettled 라는 메소드도 있습니다. Promise.all과의 차이점은 Promise.all은 중간의 하나의 프로미스라도 실패하면 바로 중단하고 에러를 리턴하는 반면 Promise.allsettled는 중간에 reject 되는 프로미스가 있더라도 프로미스 이터러블을 끝까지 순회한 뒤에 에러를 리턴한다는 것입니다.
이전 코드에서는 쿼리를 모두 async/await를 사용해 날리고 있었습니다. 즉 각각의 쿼리가 순차적으로 실행되었습니다.
async getJournalDetail(journalId: number): Promise<JournalDetail> {
const start = Date.now(); //시작 시간 카운트
const journalDogIds = await this.journalsDogsService.getDogIdsByJournalId(journalId, start);
const journalInfoRaw = await this.getJournalInfoForDetail(journalId, start);
const photoUrlsRaw = await this.journalPhotosService.getPhotoUrlsByJournalId(journalId, start);
const dogInfoRaw = await this.getDogsInfoForDetail(journalDogIds, start);
const excrements = await this.excrementsService.getExcrementsCount(journalId, journalDogIds, start);
...
return new JournalDetail(journalInfo, dogInfo, excrementsInfo);
}
await로 순차 실행하고 있습니다. async getJournalInfoForDetail(journalId: number, start: number): Promise<Partial<Journals>> {
const result = this.journalsRepository.findOne({
where: { id: journalId },
select: JournalInfoForDetail.getKeysForJournalTable(),
});
console.log('2 :', Date.now() - start);
return result;
}
결과
1 : 17
2 : 18
3 : 30
4 : 46
5 : 84
async getJournalDetail(journalId: number): Promise<JournalDetail> {
const start = Date.now();
const journalDogIds: number[] = await this.journalsDogsService.getDogIdsByJournalId(journalId, start);
const [journalInfoRaw, photoUrlsRaw, dogInfoRaw, excrements]: [
Partial<Journals>,
Partial<JournalPhotos[]>,
Partial<Dogs[]>,
any[],
] = await Promise.all([
this.getJournalInfoForDetail(journalId, start),
this.journalPhotosService.getPhotoUrlsByJournalId(journalId, start),
this.getDogsInfoForDetail(journalDogIds, start),
this.excrementsService.getExcrementsCount(journalId, journalDogIds, start),
]);
...
return new JournalDetail(journalInfo, dogInfo, excrementsInfo);
}
await으로 처리하고, 의존성을 갖지 않는 나머지 함수들은 Promise.all로 병렬 처리하였습니다. Promise.allsetteled가 아닌 Promise.all을 사용했습니다.결과
1 : 7
2 : 8
3 : 14
5 : 15
4 : 16
이를 통해 API 성능을 30ms -> 21ms로 30% 개선할 수 있었습니다.
reference:
mdn 프로미스 문서
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
자바스크립트 딥 다이브