이미지 다중 선택이 되지 않는 문제, 재고 수량 확인이 제대로 동작하지 않는 문제 모두 Promise.all을 사용하여 문제를 해결할 수 있었기에 이에 대해 꼭 정리해야겠다고 생각했다.
문제 상황
상품 등록/수정 폼의 이미지 등록 인풋에서는 이미지를 여러 개 등록할 수 있도록 구현하였다. 그런데 이미지를 두 개 이상 추가했음에도 이미지 미리보기 창에 이미지가 한 개만 보여졌다.
처음에 가장 직접적인 연관이 있을 거라 생각했던 인풋 태그를 살펴보았지만,
multiple 옵션이 잘 적용되어 있는 것을 확인하였다.
<input
{...field}
type="file"
className="hidden"
multiple
id="input-file"
onChange={addImages}
onKeyDown={onKeyDown}
/>
그래서 이번엔 addImages 함수를 살펴보았고 인풋 태그를 통해 받아온 이미지들을 콘솔에 출력해본 결과, resizeFile를 통해 리사이징된 이미지들을 받아오는 과정에서 문제가 있다는 것을 알게 되었다.
// 이미지를 등록하는 함수
async function addImages(e: ChangeEvent<HTMLInputElement>) {
e.preventDefault();
let images = e.target.files;
if (images) {
for (let i = 0; i < images.length; i++) {
const resizedImage = await resizeFile(images[i]);
previewImageUrlList.push(resizedImage);
}
setPreviewImages(previewImageUrlList);
}
}
문제 해결
resizeFile 함수 자체에는 문제가 없었고, await을 통해 비동기적으로 작동하는 부분에 문제가 있어보였다.
문제를 해결할 수 있는 방법을 찾아보다가 Promise.all()을 통해 해결할 수 있다는 것을 알게 되었다.
Promise.all()
여러개의 Promise를 동시에 처리하고, 모든 Promise가 완료될 때까지 기다린 다음 그 결과를 반환하는 데 사용된다.
Promise를 사용하여 비동기 작업을 처리할 수 있다.
예를 들어, 웹 요청을 보내거나 파일을 읽거나 데이터베이스 쿼리를 실행하는 등의 작업은 시간이 걸리므로, 해당 작업이 완료될 때까지 기다리지 않고 다음 코드가 실행되도록 할 수 있다.
그런데 때로는 여러 개의 비동기 작업이 있고, 이들이 모두 완료되어야만 다음 단계로 진행해야 하는 상황이 있다. 이때 Promise.all()이 유용하게 사용된다.
예를 들어, 다음과 같이 여러 개의 Promise를 배열로 전달하여 Promise.all()을 사용할 수 있다.
// 1초 후에 '첫 번째'를 반환하는 Promise
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, '첫 번째');
});
// 2초 후에 '두 번째'를 반환하는 Promise
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, '두 번째');
});
// 3초 후에 '세 번째'를 반환하는 Promise
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, '세 번째');
});
// 세 개의 Promise를 묶어 Promise.all()로 전달
Promise.all([promise1, promise2, promise3])
.then(values => {
// 모든 Promise가 완료될 때까지 기다린 후
// 각 프로미스의 결과를 배열로 반환
console.log(values); // ['첫 번째', '두 번째', '세 번째']
});
이를 addImages 함수에 적용하였다. (그리고 for문을 사용하는 대신, Array.from을 사용하여 코드를 더 간결하게 바꾸었다.)
Array.from(images)으로부터 생성된 배열의 각 요소에 대해 resizeFile 함수를 적용하고, map을 통해 각 요소를 변환한 후 새로운 배열로 반환한다.
그런데 이때, Promise.all을 사용하여 모든 이미지 파일에 resizeFile이 적용될 때까지(모든 이미지 파일의 리사이징이 완료될 때까지) 기다린 후, 한꺼번에 resizedImages에 담게 되는 것이다.
async function addImages(e: ChangeEvent<HTMLInputElement>) {
e.preventDefault();
let images = e.target.files;
if (images) {
const resizedImages = await Promise.all<string>(Array.from(images).map(resizeFile));
// 기존 이미지와 새로 리사이징된 이미지를 합친 후 설정
setPreviewImages((prev) => [...prev, ...resizedImages]);
}
}
문제 상황
실제 결제를 진행하기 전, 사용자가 구매하려고 선택한 상품들에 대해서 재고 수량을 미리 확인해야 하는데 이 동작이 제대로 동작하지 않았다.
재고가 0임에도 불구하고 isOutOfStock이 false를 반환해서(재고가 남아있다고 판단) 재고를 감소해서 재고가 -1개가 되는 크나큰 문제가 발생했다.
// 재고가 부족한 상품이 있는지 확인
async function checkIsOutOfStock(orderProducts: BasketProductType[]) {
let isOutOfStock = false;
for (const product of orderProducts) {
const DBProduct = await fbGetProduct(product.id);
const DBStock = DBProduct?.stock;
if (DBStock - product.quantity < 0) {
alert(`${product.name}의 상품 재고가 부족하여 구매가 불가능합니다.`);
isOutOfStock = true;
return isOutOfStock;
}
}
return isOutOfStock;
}
이것도 비동기 함수 처리가 잘못되었기 때문이었다.
코드가 좀 많이 바뀌었는데 Promise.all 처리 부분만 봐주세요..
문제 해결
이것도 마찬가지로 Promise.all을 사용해서 해결할 수 있었다.
상품의 재고를 확인하는 부분을 Promise.all로 감싸서 상품 재고 확인이 완료되어 results에 결과가 담길 때까지 다음 코드로 넘어가지 않도록 한다.
쉽게 말하면 Promise.all로 감싸진 부분의 작업이 완료될 때까지 기다리는 것이다.
async function checkIsOutOfStock(orderProducts: BasketProductType[]) {
try {
// 모든 상품의 재고를 확인하여 부족한 상품이 있는지 확인
const results = await Promise.all(
orderProducts.map(async (product) => {
const DBProduct = await fbGetProduct(product.id);
const DBStock = DBProduct?.stock;
return { isOutOfStock: DBStock - product.quantity < 0, productName: product.name };
})
);
// 결과 배열에서 하나라도 true -> 재고 부족
const outOfStockProducts = results.filter((result) => result.isOutOfStock);
if (outOfStockProducts.length > 0) {
const productName = outOfStockProducts.map((product) => product.productName).join(", ");
alert(`${productName}의 상품 재고가 부족하여 구매가 불가능합니다.`);
return true;
} else {
return false;
}
} catch (error) {
console.error("상품 재고 확인 실패:", error);
return true; // 에러 발생 시 재고 부족으로 처리
}
}