๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Frontend/React

[React] ์ฟ ํฐ ์ „์ฒด ๋ฐ›๊ธฐ ๊ธฐ๋Šฅ์„ ๊ฐœ์„ ํ•ด๋ณด์ž!

by YWTechIT 2025. 9. 15.
728x90

๐Ÿ“ ์ฟ ํฐ ์ „์ฒด ๋ฐ›๊ธฐ ๊ธฐ๋Šฅ์„ ๊ฐœ์„ ํ•ด๋ณด์ž!

๋ฌธ์ œ ์ƒํ™ฉ

์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€(PDP) ๋‚ด ์ฟ ํฐ๋ฐ›๊ธฐ ํŒ์—…์—์„œ ์ฟ ํฐ ์ „์ฒด ๋ฐ›๊ธฐ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ, ์ผ๋ถ€ ์œ ์ €๊ฐ€ ์ฟ ํฐ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ ์•Œ๋Ÿฟ์ด ๋ฐ˜๋ณต์ ์œผ๋กœ ๋…ธ์ถœ๋œ๋‹ค๋Š” CS ์ ‘์ˆ˜๋˜์—ˆ๋‹ค. ์ฆ‰, ์‚ฌ์šฉ์ž๋Š” ์ „์ฒด ์ฟ ํฐ ๋ฐ›๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋Š”๋ฐ ์‹ค์ œ๋กœ๋Š” ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ผ๋ถ€ ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋งŒ ๋ณด๋Š” ์ƒํ™ฉ

์›์ธ ๋ถ„์„

DataDog์—์„œ ์ฟ ํฐ ๋‹ค์šด๋กœ๋“œ์‹œ ํ˜ธ์ถœ๋˜๋Š” ๋ฒ ๋„คํ• API ํ™•์ธ. ์—๋Ÿฌ๋กœ๊ทธ๋ฅผ ์‚ดํŽด๋ณด๋‹ˆ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ์‹œ์ ์— 0.1์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ํ• ์ธ ์ฟ ํฐ์ด ์ด๋ฏธ ๋ฐœ๊ธ‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ๋กœ๊ทธ๊ฐ€ ์ฐํ˜€์žˆ๋Š”๊ฒƒ์„ ๋ฐœ๊ฒฌํ•จ.(01:12:00.520, 01:12:00.521, 01:12:00.522, 01:12:00.525 ...) ๊ทธ๋Ÿฌ๋‚˜ ํ”„๋กœ๊ทธ๋žจ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ด์ƒ ์œ ์ €๊ฐ€ 0.1์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ์ˆ˜ ์—†๊ธฐ๋•Œ๋ฌธ์— ์ฟ ํฐ ์ „์ฒด ๋ฐ›๊ธฐ ๋กœ์ง์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์ด๋ฅผ ๊ฐœ์„ ํ•จ

๊ฐœ์„  ๋ฐฉ์•ˆ

  1. Promise.allSettled ์‚ฌ์šฉ
  • ๊ธฐ์กด: Promise.all → ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ „์ฒด๊ฐ€ rejected ์ƒํƒœ๋˜์–ด ๊ฐ๊ฐ์˜ ์ฟ ํฐ ๋‹ค์šด๋กœ๋“œ ๊ฒฐ๊ณผ๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์—†์Œ
  • ๊ฐœ์„ : Promise.allSettled → ๊ฐ ์ฟ ํฐ์˜ ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ™•์ธ ๊ฐ€๋Šฅ

์ด๋ฅผ ํ†ตํ•ด “๋ช‡ ์žฅ์ด ์„ฑ๊ณตํ•˜๊ณ  ๋ช‡ ์žฅ์ด ์‹คํŒจํ–ˆ๋Š”์ง€”๋ฅผ ์ง‘๊ณ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
  • ๊ธฐ์กด: ์ฟ ํฐ์„ ๋‹ค์šด๋กœ๋“œํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.
  • ๊ฐœ์„ : ์ด 5์žฅ์˜ ์ฟ ํฐ ์ค‘ 4์žฅ์€ ๋ฐ›์•˜์œผ๋ฉฐ, 1์žฅ์€ ๋ฐ›๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ํ™•์ธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ฟ ํฐ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

์œ ์ €๊ฐ€ ์‹ค์ œ ์ƒํ™ฉ์„ ํˆฌ๋ช…ํ•˜๊ฒŒ ์ธ์ง€ํ•˜๋„๋ก ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐœ์„ 

  1. ์‹คํŒจ์‹œ ์ฟ ํฐ ๋ชฉ๋ก ์ตœ์‹ ํ™”: ๋‹ค์šด๋กœ๋“œ ์‹คํŒจํ•œ ์ฟ ํฐ์€ ์—ฌ์ „ํžˆ ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ๋‚จ์•„ ๋‹ค์‹œ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ์‹œ refetch()๋ฅผ ํ†ตํ•ด ์ตœ์‹  ์ฟ ํฐ ๋ชฉ๋ก์„ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ์ˆ˜์ •

์ฝ”๋“œ ๋น„๊ต

// AS-IS
  const handleDownloadAllExtraCoupons = useCallback(() => {
    try {
      await Promise.all(downloadableCouponIdList.map((couponId) => fetchCouponDownload(couponId)))

      setAlert({
        open: true,
        message: `${downloadableCouponIdList.length}์žฅ์˜ ์ฟ ํฐ์ด ๋‹ค์šด๋กœ๋“œ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž๋™์œผ๋กœ ์ตœ๋Œ€ ํ˜œํƒ ์ฟ ํฐ์ด ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`,
      })

      setCouponDefinitions((coupons) => {
        const newCoupons = coupons.map((coupon) =>
          downloadableCouponIdList.includes(coupon.id) ? { ...coupon, downloaded: true } : coupon,
        )

        return newCoupons
      })
    } catch {
      setAlert({
        open: true,
        message: '์ฟ ํฐ์„ ๋‹ค์šด๋กœ๋“œํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
      })
    }
  })

// TO-BE
 const handleDownloadAllExtraCoupons = useCallback(async () => {
    let successfulCouponDownloadCount = 0  // ๊ฐ’์ด ๋ฐ”๋€Œ์–ด๋„ ์œ ์ €๋ทฐ์— ๋ฆฌ๋ Œ๋”๋ง ํ•˜์ง€์•Š์•„๋„ ๋˜๊ณ , ์ปจํ…์ŠคํŠธ ๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์ž„์‹œ๊ฐ’์ด๋ฏ€๋กœ useState ๋Œ€์‹  let ์‚ฌ์šฉ
    let failedCouponDownloadCount = 0

    try {
      const downloadAllCouponsResponse = await Promise.allSettled(
        downloadableCouponIdList.map(async (couponId) => {
          const response = await fetchCouponDownload(couponId)

          if (response.status === 200) {
            setCouponDefinitions((coupons) => {
              const newCoupons = coupons.map((coupon) =>
                coupon.id === couponId ? { ...coupon, downloaded: true } : coupon,
              )

              return newCoupons
            })
          }

          return response
        }),
      )

      successfulCouponDownloadCount = downloadAllCouponsResponse.filter(
        (coupon) => coupon.status === 'fulfilled' && coupon.value.status === 200,
      ).length

      failedCouponDownloadCount = downloadAllCouponsResponse.filter(
        (coupon) =>
          (coupon.status === 'fulfilled' && coupon.value.status !== 200) ||
          coupon.status === 'rejected',
      ).length

      if (failedCouponDownloadCount > 0) {  // ์‹คํŒจํ•œ ์ฟ ํฐ์ด ์กด์žฌํ•˜๋Š”๊ฒฝ์šฐ catch์ ˆ๋กœ ์ด๋™ํ•˜๊ธฐ์œ„ํ•ด throw error
        throw new Error(
          `Some coupons failed to download. (Total: ${downloadableCouponIdList.length}, Success: ${successfulCouponDownloadCount}, Failed: ${failedCouponDownloadCount})`,
        )
      }

      setAlert({
        open: true,
        message: `${successfulCouponDownloadCount}์žฅ์˜ ์ฟ ํฐ์ด ๋‹ค์šด๋กœ๋“œ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž๋™์œผ๋กœ ์ตœ๋Œ€ ํ˜œํƒ ์ฟ ํฐ์ด ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`,
      })
    } catch {
      setAlert({
        open: true,
        message: `์ด ${downloadableCouponIdList.length}์žฅ์˜ ์ฟ ํฐ ์ค‘ ${successfulCouponDownloadCount}์žฅ์€ ๋ฐ›์•˜์œผ๋ฉฐ, ${failedCouponDownloadCount}์žฅ์€ ๋ฐ›๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.<br/>ํ™•์ธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ฟ ํฐ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.`,
        onConfirm: () => {
          refetch() // ์ฟ ํฐ ๋ชฉ๋ก์„ ์ตœ์‹ ํ™”ํ•˜๊ธฐ์œ„ํ•ด refetch ์‚ฌ์šฉ
        },
      })
    }
  }, [downloadableCouponIdList, setAlert, setCouponDefinitions, refetch])

๊ฒฐ๋ก 

  1. Promise.allSettled์„ ์‚ฌ์šฉํ•ด ๋‹ค์šด๋กœ๋“œ ์„ฑ๊ณต/์‹คํŒจ๋ฅผ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ถ”์ ํ•จ
  2. ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์‹คํŒจํ•œ ์ด์œ ์™€ ํ˜„ํ™ฉ์„ ์นœ์ ˆํ•˜๊ฒŒ ์•ˆ๋‚ด
  3. ์ฟ ํฐ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ ์‹œ์—๋Š” ์ฟ ํฐ ๋ชฉ๋ก์„ ์ตœ์‹ ํ™”ํ•˜์—ฌ ๋ฐ˜๋ณต ์˜ค๋ฅ˜ ๋ฐฉ์ง€

์ด ๊ณผ์ •์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)์„ ๊ฐœ์„ ํ•˜๊ณ , ๋ถˆํ•„์š”ํ•œ CS ๊ฐ์†Œ๋ฅผ ๊ธฐ๋Œ€ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€