프로미스는 비동기로 실행되는 작업의 완료 상태와 리턴값, 완료시 실행될 콜백 함수를 저장하는 객체입니다.
비동기 작업이 완료되기 전에 미리 생성할 수 있어 비동기 함수가 동기 함수처럼 값을 리턴할 수 있게 해줍니다.
프로미스의 상태(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
자바스크립트 딥 다이브