
많은 JavaScript강의에서 async, await 문법을 가르치기 전에 Promise에 대해서 간단하게 살펴보고 지나가는데 Promise만의 디테일한 사용법과 다양한 메서드를 사용해보고 싶어서 정리하는 글이다.
Promise는 비동기 연산이 최종적으로 성공했는지 실패했는지를 알려주는 객체이다.
비동기 작업이 끝날 때까지 결과를 기다리는 것이 아닌, 결과를 제공하겠다는 Promise를 즉, 약속을 반환한다는 의미에서 Promise라고 이름이 지어졌다.
Promise의 반환값은 3가지가 있다.
PendingfulfilledRejected
// Promise를 이용한 로직 구성
const fakeRequest = (url) => {
return new Promise((resolve, reject) => {
const delay = Math.random();
setTimeout(() => {
if (delay > 0.5){
resolve("complete worked");
}else{
reject("fail worked");
}
},1000);
})
};
fakeRequest("book/com")
.then((response) => {
console.log(response);
return fakeRequest("book.com/page2");
})
.then((response) => {
console.log(response);
return fakeRequest("book.com/page3");
})
.then((response) => {
console.log(response);
})
.catch((e) => {
console.log(e);
})
// 아래 로직은 위와 동일한 로직이지만 콜백으로 구성
// fakeRequest("book.com/", (reponse) => {
// console.log(reponse);
// fakeRequest("book.com/page2", (response) => {
// console.log(response);
// fakeRequest("book.com/page3", (response) => {
// console.log(response);
// }, (response) => {
// console.log(response);
// })
// }, (response) => {
// console.log(response);
// })
// }, (response)=> {
// console.log(response);
// })
콜백 함수로만 작성한거와 Promise를 쓴 요청연쇄 처리를 비교해보자
한눈에 보이듯 Promise로 처리한 요청이 훨씬 간결하다.
.then은 .then콜백에서 Promise를 반환할 때마다 연쇄된다는 점(메서드 체이닝)을 이용하는 것이다.
.then은 요청의 응답이 성공적으로 들어올 시 실행되는 구간이다.
.catch은 요청의 응답이 실패할 시 실행되는 구간이다.
성공했을 시에는 return 값으로 다음 요청에 해당 전달하고자 하는 데이터를 전달해줄 수 있다.
만약 중간에 실패한다면 .catch로 넘어가게 되면서 요청을 중단하게 된다.
그럼 왜 Promise객체를 함수로 만들어서 하는걸까?
재사용성
Promise객체로 만들면 해당요청이 필요할 때마다 함수를 호출하게 되므로 반복되는 비동기 작업을 효율적으로 처리할 수 있다.
확장성
Promise객체를 함수로 만들면 인자를 전달하여 동적으로 비동기 작업을 수행할 수 있다.
또한 여러개의 Promise객체를 반환하는 함수들을 연결하여 복잡한 비동기 로직 구현이 가능하다.
가독성
Promise객체를 함수로 만들면 코드의 각 부분에 대해서 역할이 명확해져서 코드의 가독성이 높아진다.
Promise가 생성된다면 해당 결과는 then, catch, finally 핸들러를 통해 받은 다음 작업을 진행할 수 있다.
즉, Promise핸들러는 Promise의 상태에 따라 실행되는 콜백함수라고 보면 된다.
Promise가 fulfilled 되었을 때 실행할 콜백 함수를 등록하고, 새로운 Promise을 반환
Promise가 rejected되었을 때 실행할 콜백함수를 등록하고, 새로운 Promise를 반환
fulfilled되었거나 rejected될 때 상관없이 실행할 콜백함수를 등록하고, 새로운 Promise를 반환Promise객체는 여러가지 정적 메서드를 제공한다.
정적메서드는 Promise객체를 초기화 하거나 생성하지 않고도 바로 사용할 수 있기 때문에 비동기 처리를 효율적이고 간단하게 구현할 수 있다.
resolve메서드는 주어진 값으로 이행하는 Promise.then객체를 반환한다.
해당 값이 Promise인 경우 해당 Promise가 반환된다.
만약 해당 반환 값이 thenable인 경우 반환된 Promise는 해당 thenable을 따르며 해당 로직의 최종 상태를 취한다.
const promise1 = Promise.resolve(123);
promise1.then((value) => {
console.log(value);
});

주어진 이유로 거부된 Promise객체를 반환한다.
주어진 이유로 거부된은 만약 Promise로직 안에 API요청이 실패한다면 reject로 반환된다.
const promise = Promise.reject(new Error("error working"));
promise
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
});

all메서드는 순회 가능한 객체에 주어진 모든 Promise를 이행한 후, Promise가 주어지지 않을 때 이행하는 Promise를 반환한다.
주어진 Promise중 하나가 거부하는 경우, 첫번째로 거절한 Promise의 이유를 사용해 자신도 거부한다.
all메서드는 배열, Map, Set에 포함된 여러개의 Promise요소들을 한꺼번에 비동기 작업 처리해야 할 때 유용한 Promise정적 메서드이다.
즉, 모든 Promise비동기 처리가 fulfilled될때까지 기다려서, 모든 Promise가 완료되면 그 때 then핸들러를 실행하는 형태로 보면 된다.
가장 대표적인 사용 예시로는 여러개의 API요청을 보내고 모든 응답을 받아야 하는 경우에 사용할 수 있다.
const p1 = Promise.resolve(3);
const p2 = 1337;
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 100);
});
Promise.all([p1, p2, p3]).then((values) => {
console.log(values); // [3, 1337, "foo"]
});

위 예시를 보면 all메서드에 여러개의 요청을 넣어놔서 해당 요청에 대해서 한번에 응답을 받아야하는 경우에 사용하는 로직을 볼 수 있다.
allSettled는 주어진 모든 Promise를 이행하거나 거부한 후, 각 Promise에 대한 결과를 나타내는 객체 배열을 반환한다.
일반적으로 서로의 성공 여부에 관련없는 여러 비동기 작업을 수행해야 하거나, 항상 각 Promise의 실행결과를 알고 싶을때 사용한다.
allSettled에 비해all은 반환한 Promise는 서로 연관된 작업을 수행하거나, 하나라도 거부 당했을 때 즉, 즉시 거부할 때 적합하다.
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
setTimeout(reject, 100, "foo")
);
const promises = [promise1, promise2];
Promise.allSettled(promises).then((results) =>
results.forEach((result) => console.log(result.status))
);

any메서드는 all메서드와는 반대 버전으로, all이 주어진 모든 Promise가 모두 완료해야만 결과를 도출한다면, any는 주어진 Promise중 하나돌 fulfilled된다면 바로 반환하는 메서드이다.
const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "quick"));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, "slow"));
const promises = [promise1, promise2, promise3];
Promise.any(promises).then((value) => console.log(value));

race메서드는 Promise객체를 반환한다.
해당 Promise객체는 iterable안에 있는 Promise중에 가장 먼저 완료된 것의 결과값으로 그대로 이행하거나 거부한다.
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "one");
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "two");
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
});

const myPromise = new Promise((resolve, reject) => {
// 비동기 작업 시뮬레이션 (예: setTimeout을 사용한 비동기 작업)
setTimeout(() => {
const randomValue = Math.random();
if (randomValue < 0.5) {
resolve(randomValue);
} else {
reject(new Error('Random value is greater than or equal to 0.5'));
}
}, 1000);
});
myPromise
.then((result) => {
console.log('Step 1:', result);
return result * 2;
})
.then((result) => {
console.log('Step 2:', result);
return result + 3;
})
.then((result) => {
console.log('Step 3:', result);
return result ** 2;
})
.then((result) => {
console.log('Step 4:', result);
return result - 10;
})
.then((result) => {
console.log('Step 5:', result);
return result / 2;
})
.then((result) => {
console.log('Step 6:', result);
})
.catch((error) => {
console.error('Error:', error.message);
});
Promise도 콜백 못지않게 지나치게 많은 Promise메서드 체이닝이 반복되면 코드의 가독성이 떨어진다는 단점이 있다.
catch문조차 모든 then체이닝에 대해서 한 곳에서 처리하다 보니 중간에 에러가 발생하면 해당 에러에 대해서 대응하기가 상당히 힘들다
이를 위해서 나온 문법이 바로 async, await이다.
ES8에서 도입된 비동기 처리를 위한 문법으로 비동기 작업을 동기작업처럼 쓸 수 있도록 도와주는 문법이다.