그간 자바스크립트에서는 ES6+에서 지원하는 프로미스(Promise)객체를 사용해 비동기 프로그래밍을 할 때 동기 프로그래밍 방식으로 코드를 작성할 수 있었다. 프로미스는 JS에서 콜백을 해결하고자 하는 문제가 아니라, 비동기가 발생하는 시점을 값으로 다루며 변수에 담을 수 있는 객체로 바라볼 수 있게 해준다. 아래에서는 프로미스(Promise) 사용법과 async awiat 사용법을 다시 돌아보고, ES2020에서 새로 추가된 Promise.allSettled
에 대해 알아보자.
프로미스는 세 가지 상태를 갖는다.
대기중(pending)
: 결과를 기다리는 중이행됨(fulfilled)
: 수행이 정상적으로 끝났고 결과값을 가지고 있음거부됨(rejected)
: 수행이 비정상적으로 끝났음위의 세 가지 상태 중에서 이행됨(fulfilled)
과 거부됨(rejected)
상태를 처리됨(settled)
상태라고 부른다.
then은 처리됨(settled)
상태가 된 프로미스를 처리할 때 사용하는 메서드다. 프로미스가 처리된(settled)
상태가 되면 then 메서드의 인수로 전달된 함수가 호출된다. 그리고 then 메서드는 항상 새로운 프로미스 객체를 반환한다. 이때 반환하는 프로미스 객체는 내부 함수가 반환한 값이다. return
키워드를 적어주지 않으면 프로미스 객체는 undefined
를 반환한다는 것을 유의하자.
catch는 프로미스 수행중 발생한 예외를 처리하는 메서드다. catch 메서드도 마찬가지로 새로운 프로미스 객체를 반환한다. 참고로, 프로미스 예외처리를 할 때는 반드시 then이나 catch 메서드 안쪽에서 함수를 작성해야 한다.
finally는 프로미스가 처리된(settled) 상태일 때 호출되는 메서드다. 프로미스 체인의 가장 마지막에 사용된다. then 메서드와 유사하지만, 이전에 사용한 프로미스를 그대로 반환한다는 점이 다르다. 처리된 프로미스의 데이터를 건들이지 않고 추가작업(서버에 로그 보내기 등)을 할 때 유용하다.
Promise.all()은 여러개의 프로미스를 병렬로 처리할 때 사용하는 함수이다. then 메서드를 체인형태로 연결하면 각각의 비동기 처리 요청이 병렬로 처리되지 않고, 순차적으로 실행된다. 비동기 함수 간의 의존성이 없다면 then 메서드를 사용해 순차적으로 처리하기보다 병렬로 처리하는 것이 더 빠르다.
// 순차적으로 실행되는 비동기 코드
updateNameRequest()
.then(data => {
console.log(data);
return updateProfileRequest();
})
.then(data => {
console.log(data);
});
// 위의 비동기 함수를 각각 호출하면 병렬로 처리된다
updateNameRequest().then(data => console.log(data));
updateProfileRequest().then(data => console.log(data));
// 위의 코드처럼 여러 프로미스를 병렬로 처리하고 싶다면 Promise.all을 쓰면 된다.
Promise.all([updateNameRequest(), updateProfileRequest()]).then(([data1, data2]) => {
console.log(data1, data2)
});
async await
은 ES2017에서 JS 표준이 되었다. 이 슈가 신택스를 사용해서 비동기 코드를 작성하면 프로미스의 then 메서드를 체인 형식으로 호출하는 것보다 직관적인 코드를 작성할 수 있어 가독성이 좋아진다. async await
은 슈가신택스에 가까우며, 프로미스가 이보다 더 큰 개념이다. Promise
는 객체 형태인 반면, async await
은 함수에 적용되는 개념이다. 이 함수는 프로미스 객체를 반환한다.
// 서로 의존성이 있는 비동기 함수를 Promise.all() 메소드를 사용해 여러개의 프로미스를 병렬로 처리하는 코드
function requestPromise() {
return updateNameRequest()
.then(data1 => Promise.all([data1, updateAddressRequest]))
.then(([data1, data2]) => return updateProfileRequest(data1, data2);
});
}
// 위의 코드를 async await 함수로 변경한 코드
async function requestPromise() {
const data1 = await updateNameRequest();
const data2 = await updateAddressRequest();
return updateProfileRequest(data1, data2);
}
// try catch문으로 예외처리하는 코드 추가
async function requestPromise() {
try {
const data1 = await updateNameRequest();
const data2 = await updateAddressRequest();
return updateProfileRequest(data1, data2);
} catch (error) {
console.log(error)
}
}
// 두 개의 프로미스를 먼저 생성하고 await 키워드를 나중에 사용
async function getData() {
const updateLike = asyncLike();
const updateCart = asyncCart();
const likeData = await updateLike;
const cartData = await updateCart;
}
// 위의 코드를 Promise.all()을 사용해 리팩토링
async function getData() {
const [likeData, cartData] = await Promise.all([asyncLike(), asyncCart()]);
}
MDN: Promise.allSettled()
(참고로 구형 브라우저나, Node.js에서 사용한다면 Pollyfil을 따로 구현해줘야한다.)
JavaScript의 ES2020 버전에 allSettled
라는 프로미스 메소드가 추가되었다. 이 메소드는 배열이나 객체를 통해 나열된 Promise의 집합이 모두 이행(resolve)되거나 거부(reject)했을 때에 대한 대응을 할 수 있는 Promise
객체를 반환한다.
여러개의 프로미스 요청을 병렬로 처리하려면 Promise.all()
메소드를 사용하면 됐다. 그런데 Promise.all()
메소드는 하나가 실패하면 모두 실패해야 하는 상황에서 쓰면 좋지만, 도중에 하나의 요청이 실패하면 이후 요청의 결과를 알 수 없다.
Promise.allSettled()
메소드는 모든 Promise 요청의 처리 결과를 받을 수 있다. 하나의 요청이 실패하더라도 다른 요청의 응답들은 사용해야 할 때 쓰면 좋다.
리액트로 마이페이지 예제코드를 간단히 만들어보았다.
const myPage = () => {
const [username, setUsername] = useState("")
const [address, setAddress] = useState("")
const onSubmit = () => {
getUsername(username);
getAddress(address)
}
return (
// ...
)
}
예외 처리하는 코드를 추가해보자
const myPage = () => {
const [username, setUsername] = useState("")
const [address, setAddress] = useState("")
const [errorMessage, setErrorMessage] = useState();
const onSubmit = () => {
Promise.all([getUsername(username),
getAddress(address)])
.catch(errorMessage => setErrorMessage(error));
}
return (
// ...
)
}
Promise.all()
메소드는 프로미스 중 하나라도 거부되면 거부가 된 첫 프로미스에서 catch
핸들러를 호출한다. 거부된(rejected) 다른 프로미스는 무시되고, 최초발생한 단 하나의 오류만 처리할 수 있다.Promise.allSettled()
메소드는 거부(rejected) 여부에 관계없이 모든 프로미스의 응답을 기다리고 결과를 처리하며, 개별적인 프로미스에 대한 상태를 배열로 반환해준다. const myPage = () => {
const [username, setUsername] = useState("")
const [address, setAddress] = useState("")
const [errorMessage, setErrorMessage] = useState();
const onSubmit = () => {
Promise.allSettled([getUsername(username),
getAddress(address)])
.then(results => {
const rejectedPromises = results.filter(({status}) => status === "rejected");
const errorMessages = rejectedPromises.map(({reason}) => reason)
setErrorMessages(errorMessages);
})
}
return (
// ...
)
}