Node.js-반복문에서의 콜백함수 동기화

4 분 소요

평문에서의 async/await

aysnc/await를 통해 개발을 하면서 콜백함수의 지옥에서 벗어날 수 있었고 좀 더 보기 편하고 직관적인 코드를 짤 수 있었다.
리턴타입이 Promise에 감싸져있는 것이 아닌 본래의 값을 가져와서 데이터를 뽑아낼 수 있어 굉장히 편했다.
아래의 코드와 같이 async/await를 사용했다.

const addEvent = async (req: Request, res: Response, next: NextFunction) => {
	const { title, content, userName } = req.body;
	try {
		const event = await Event.findOne({ title, content, userName });
		if (event) res.status(201).json(event);
		else res.status(404);
	} catch (err) {
		console.log(err);
		next(err);
	}
};
const getEvent = async (req: Request, res: Response, next: NextFunction) => {
	const { eventId } = req.params;
	try {
		const event = await Event.findById(eventId);
		if (event) res.status(201).json(event);
		else res.status(404);
	} catch (err) {
		console.log(err);
		next(err);
	}
};

반복문에서의 async/await사용 시 문제점 봉착

개발을 하던 도중 맞닥뜨린 상황은 다음과 같다.
상품에 대한 재고와 옵션가격을 수정하는 과정에서 프론트로부터 받아오는 request body가 배열로 들어오고 나는 이 배열 속에서 작업을 해야했다.
배열 속에 들어가 해당 재고 객체의 색깔과 사이즈가 같다면 수정작업을, 없다면 생성작업을 해야했다. 나의 기존 코드는 아래와 같았다.

// POST -> 상품 수정하기
export const modifyProduct = async (req: Request, res: Response, next: NextFunction) => {
    const stockIdList: string[] = [];
    const sizeIdList: string[] = [];
    const { productId, firstCategoryId, secondCategoryId, name, price, discountRate, tags, colors, fabric, modelFitting, laundry, description, createdAt, sizeList, stockList } = req.body;
    const { an, bi, sin, du, fit } = req.body.quality;
    const { height, weight, top, bottom, shoes } = req.body.model;
    
    try {
        stockList.forEach((stock: StockDocument) => {
            const { color, size, optionPrice, stockNum } = stock;
            Stock.findOne({ productId, color, size }, async (err, res) => {
                if (err) console.log(err);
                if (res) {
                    await Stock.findByIdAndUpdate(res._id, { color, size, optionPrice, stockNum });
                    stockIdList.push(res._id);
                    
                } else {
                    const newStock = Stock.create({ color, size, optionPrice, stockNum, productId });
                    stockIdList.push((await newStock)._id);  
                }
                console.log(`1st stockIdList : ${stockIdList}`);
                await Product.findByIdAndUpdate(productId, { stockIdList });
            });
            console.log(`2nd stockIdList : ${stockIdList}`);
        });
        console.log(`3rd stockIdList : ${stockIdList}`);
        sizeList.forEach((size: SizeDocument) => {
            const { name, total, shoulder, chest, arm, waist, thigh, mitWi, mitDoole, width, length, breadth } = size;
            Size.findOne({ productId, name }, async (err, res) => {
                if (err) console.log(err);
                if (res) {
                    await Size.findByIdAndUpdate(res._id, { total, shoulder, chest, arm, waist, thigh, mitWi, mitDoole, width, length, breadth });
                    sizeIdList.push(res._id);
                } else {
                    const newSize = Size.create({ name, total, shoulder, chest, arm, waist, thigh, mitWi, mitDoole, width, length, breadth, productId });
                    sizeIdList.push((await newSize)._id);  
                }
                await Product.findByIdAndUpdate(productId, { sizeIdList });
            });
        });
    } catch (err) {
        console.log(err);
        next(err);
    }
    
    console.log(`4th stockIdList : ${stockIdList}`);
    Product.findByIdAndUpdate(productId, { firstCategoryId, secondCategoryId, name, price, discountRate, tags, colors, fabric, modelFitting, laundry, description, createdAt }, async (err, res) => {
        if (err) console.log(err);
        if (res) {
            await Model.findByIdAndUpdate(res.modelId, { height, weight, top, bottom, shoes });
            await Quality.findByIdAndUpdate(res.qualityId, { an, bi, sin, du, fit });
        }
    });
    console.log(`5th stockIdList : ${stockIdList}`);
    res.status(201).json({ message: '성공적으로 수정했습니다.' });
};

위 코드의 문제점은 선언해놓은 stockIdList, sizeIdList 배열의 원소들이 반복문 내부에서 들어와야하는 시점이 비동기적으로 처리되어 Product.findByIdAndUpdate() 함수가 실행되는 시점에는 아직 배열에 원소가 전부 들어오지 못한 채로 있다는 것이다.
해당 함수를 라우팅해서 실행하게 되면 DB엔 매번 stockIdList, sizeIdList 필드가 다른 갯수로 랜덤하게 저장되어진다.
게다가 라우팅에 대한 응답으로 정상적으로 201 상태코드와 성공적으로 수정되었다는 메세지를 응답한다.

해결 방법 Promise.all()

해당 문제를 해결하기 위해 Promise.all()을 이용해 모든 promise가 완료될 때까지 await하는 방법을 채택했다.
수정된 코드는 아래와 같다.

// PUT -> 상품 수정하기
export const modifyProduct = async (req: Request, res: Response, next: NextFunction) => {
    const { productId, firstCategoryId, secondCategoryId, name, price, discountRate, tags, colors, fabric, modelFitting, laundry, description, sizeList, stockList } = req.body;
    const { an, bi, sin, du, fit } = req.body.quality;
    const { height, weight, top, bottom, shoes } = req.body.model;
    try {
        const modifyStock = async (stock: StockDocument): Promise<string> => {
            const { color, size, optionPrice, stockNum } = stock;
            const result = await Stock.findOne({ productId, color, size });
            if (result) {
                const updatedStock = await Stock.findByIdAndUpdate(result._id, { optionPrice, stockNum });
                return updatedStock?._id;
            } else {
                const newStock = await Stock.create({ productId, color, size, optionPrice, stockNum });
                return newStock._id;
            }
        };
        const modifySize = async (size: SizeDocument): Promise<string> => {
            const { name, total, shoulder, chest, arm, waist, thigh, mitWi, mitDoole, width, length, breadth } = size;
            const result = await Size.findOne({ productId, name });
            if (result) {
                const updatedSize = await Size.findByIdAndUpdate(result._id, { name, total, shoulder, chest, arm, waist, thigh, mitWi, mitDoole, width, length, breadth });
                return updatedSize?._id;
            } else {
                const newSize = await Size.create({ productId, name, total, shoulder, chest, arm, waist, thigh, mitWi, mitDoole, width, length, breadth });
                return newSize._id;
            }
        };

        const getStockList = async (stockList: StockDocument[]) => {
            const promises = stockList.map(stock => modifyStock(stock));
            return await Promise.all(promises);
        };
        
        const getSizeList = async (sizeList: SizeDocument[]) => {
            const promises = sizeList.map(size => modifySize(size));
            return await Promise.all(promises);
        };

        const resultStockIdList = await getStockList(stockList);
        const resultSizeIdList = await getSizeList(sizeList);

        const updatedProduct = await Product.findByIdAndUpdate(productId, {
            stockIdList: resultStockIdList,
            sizeIdList: resultSizeIdList,
            firstCategoryId, 
            secondCategoryId, 
            name, 
            price, 
            discountRate, 
            tags, 
            colors, 
            fabric, 
            modelFitting, 
            laundry, 
            description, 
        });
        await Model.findByIdAndUpdate(updatedProduct?.modelId, { height, weight, top, bottom, shoes });
        await Quality.findByIdAndUpdate(updatedProduct?.qualityId, { an, bi, sin, du, fit });
        res.status(201).json({ message: '성공적으로 수정했습니다.' });
    } catch (err) {
        console.log(err);
        next(err);
    }
};

getStockList()함수를 await하여 받아낸 resultStockIdList 배열에는 모든 프로미스가 끝나고 값을 리졸브한 상태의 타입의 원소들이 들어가 있기 때문에 비로소 원하는 동작이 이루어진다.

카테고리:

업데이트:

댓글남기기