๐Ÿš€ [React] React16๊ณผ Suspense, ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ 1


๊ฐœ์š”

Suspense๋Š” ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ๊นŒ?

ErrorBoundary๊ฐ€ error๋ฅผ ๊ฐ์ง€ํ•ด์„œ throw ํ•˜๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ, Suspense๋Š” Promise๋ฅผ ๊ฐ์ง€ํ•œ๋‹ค.

Suspense ๋Š” ์™œ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์„๊นŒ?

๋ฌด์—‡๊ณผ ์–ด๋–ป๊ฒŒ๋ฅผ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ๋Š” ๋ฐ์ดํ„ฐ ํŽ˜์นญ์— ๊ด€๋ จ๋œ ์ƒํƒœ(๋กœ๋”ฉ, ์‹คํŒจ, ์„ฑ๊ณต)๋ฅผ ๊ด€๋ฆฌํ•˜๊ฑฐ๋‚˜ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

โ†’ ์„ ์–ธ์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ(์–ด๋–ค๊ฑธ ๊ฐ€์ง€๊ณ  ๋ฌด์—‡์„ ํ• ์ง€)์ด ๊ฐ€๋Šฅํ•ด์ง

๋Œ€์ˆ˜์  ํšจ๊ณผ์™€ Suspense

  1. ๋Œ€์ˆ˜์  ํšจ๊ณผ๋Š” ๊ฐ์‹ธ๊ณ  ์žˆ๋Š” ํ•จ์ˆ˜์˜ ๋กœ์ง์ด ๊ฐ์‹ธ์ง„(๋‚ดํฌํ•˜๋Š”) ํ•จ์ˆ˜์˜ ์—ญํ• ์„ ๋ถ„๋ฆฌํ•  ๋•Œ ์‹คํ˜„๋œ๋‹ค.
  2. React์˜ Suspense๋Š” ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๋Š” ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ๋กœ๋”ฉ UI ํ‘œ์‹œ๋ผ๋Š” ์—ญํ• ์„ ๋ถ„๋ฆฌํ•˜๊ณ  ์žˆ๋‹ค.
  3. React์˜ Suspense์˜ ์ฐฝ์•ˆ ์›๋ฆฌ๋Š” ๋Œ€์ˆ˜์  ํšจ๊ณผ์ด๋‹ค.

๋ณธ๋ก 

React 18 ์ด์ „์˜ Suspense๋Š” Data Fetching์„ ์œ„ํ•œ Pending Handler๊ฐ€ ์•„๋‹ˆ๋ผ, ๊ธฐ์กด์˜ ์›Œํ„ฐํด ๋ฐฉ์‹์œผ๋กœ ์ด๋ฃจ์–ด์ ธ์žˆ๋Š” Render๋ฐฉ์‹์„ ๊ฐœ์„ ํ•ด์ฃผ๋Š” ์—ญํ• ์ด๋‹ค. https://sangminnn.tistory.com/76

ํ•„์š”ํ•œ ๊ตฌ์กฐ

  • promise๋ฅผ ๊ฐ์ง€ํ•ด์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ suspended ์ƒํƒœ๋กœ ๋งŒ๋“ค์–ด ์ค„ ์žฅ์น˜ (data fetching์„ ์œ„ํ•œ suspense)
    • react-query์˜ suspense ์˜ต์…˜ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๋ ค๊ณ  ํ•จ
    • ์ด๋ฏธ์ง€, dynamic import, fetching์„ ๋ชจ๋‘ ์ง€์›
    • ์ดํ›„ api ํ˜ธ์ถœํ•  ๋•Œ ์œ„์˜ ์žฅ์น˜๋ฅผ ํ†ตํ•ด ํ˜ธ์ถœํ•ด์•ผ ํ•จ
  • ์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ

โ†’ @toss/async-boundary

์ฐธ๊ณ  ๋ธ”๋กœ๊ทธ

WrappedPromise(createPromiseResource) โ†’ useFetch ํ›…์œผ๋กœ ๊ณ ๋„ํ™”

// suspense์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด, ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ํŒจ์น˜ ์‹œ ์ด ์œ ํ‹ธ ์‚ฌ์šฉ
const createPromiseResource = (promise) => {
  let status = "pending";
  let result = null;

  const suspender = promise.then(
    (res) => {
      status = "fulfilled";
      result = res;
    },
    (err) => {
      status = "rejected";
      result = err;
    }
  );

  return {
    read() {
      switch (status) {
        case "pending":
          throw suspender;
        case "fulfilled":
          throw result;
        case "rejected":
          throw result;
        default:
          break;
      }
    },
  };
};

export default createPromiseResource;
// ๋น„๋™๊ธฐ ํ˜ธ์ถœ์„ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
import React, { useState, useEffect } from "react";

import { getUser } from "../apis";
import createPromiseResource from "../utils/createPromiseResource";

// ์—ฌ๊ธฐ์„œ react-query๋‚˜ axios๋กœ ๋น„๋™๊ธฐ fetch ์ž‘์—… ์ง„ํ–‰
// https://6391fa92b750c8d178d35d54.mockapi.io/api/profile/:id

const useResource = (id) => createPromiseResource(getUser(id)).read();

function UserItem({ id }) {
  const data = useResource(id);

  return <div>์ด๋ฆ„: {data.name}</div>;
}

export default UserItem;

ErrorBoundary

import React from "react";

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props); // props๋กœ errorFallback์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ
    this.state = {
      error: null,
    };
  }

  static getDerivedStateFromError(error) {
    console.log("getDerivedStateFromError");
    return { error };
  }

  componentDidCatch(err, info) {
    console.log("componentDidCatch", err, info);
    this.setState({
      error: err,
    });
  }

  render() {
    if (this.state.error) {
      return this.props.fallback ?? <h3>์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค :(</h3>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

1์ฐจ ์‹œ๋„ ์ฝ”๋“œ์—์„œ ๋ฌธ์ œ์ 

  1. ๋น„๋™๊ธฐ ํ˜ธ์ถœ์„ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ createPromiseResource ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ,
    • ์ฆ‰์‹œ ์‹คํ–‰ ํ•จ์ˆ˜๋กœ getUser(id)๋ฐ›๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ณ„์†ํ•ด์„œ ์‹คํ–‰๋˜๊ณ  ์žˆ์—ˆ์Œ

  2. ๋ถ€๋„๋Ÿฝ๊ฒŒ๋„ ๊ทผ๋ณธ์ ์œผ๋กœ .then().catch()๋ฌธ์„ ์ž˜๋ชป ์“ฐ๊ณ  ์žˆ์—ˆ์Œ(๋ฌธ๋ฒ• ์—๋Ÿฌ)
  3. 1์˜ ๋ฌธ์ œ์ ์„ ํ•ด๊ฒฐํ•˜๋ฉด์„œ ๊ทผ๋ณธ์ ์ธ ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ๋Š”๋ฐ, createPromiseResource ์œ ํ‹ธ์„ ๋ฆฌ์•กํŠธ ๋ฐ–์— ๋นผ๋‘๊ธด ํ–ˆ์ง€๋งŒ ์ฆ‰์‹œ ์‹คํ–‰ ํ•จ์ˆ˜๋กœ ๋งŒ๋“ค์—ˆ๊ธฐ ๋•Œ๋ฌธ์— (.read()) ๋ฆฌ์•กํŠธ ๋‚ด๋ถ€์—์„œ ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋˜๊ณ  ์žˆ์—ˆ์Œ

    โ†’ status๊ฐ€ pending์ธ ์ƒํƒœ๊ฐ€ ๊ณ„์† ์ƒ์„ฑ (์›๋ž˜๋Š” ํ•œ ๋ฒˆ๋งŒ ๋งŒ๋“ค์–ด์ง€๊ณ  ๊ทธ status์˜ ์ƒํƒœ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜์–ด์•ผ ํ•จ)

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. ์•„๋ž˜์™€ ๊ฐ™์ด ์ฆ‰์‹œ ์‹คํ–‰ ํ•จ์ˆ˜๋กœ ์‚ฌ์šฉํ•œ ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•จ

        
     // ๋น„๋™๊ธฐ ํ˜ธ์ถœ์„ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
     const useResource = (id) => createPromiseResource(() => getUser(id));
        
     function UserItem({ id }) {
       const { data } = useResource(id).read(); // read()๋Š” ์œ„์— ์žˆ์œผ๋‚˜ ์—ฌ๊ธฐ ์žˆ์œผ๋‚˜ ์ฐจ์ด ์—†์Œ
        
       return <div>์ด๋ฆ„: {data.name}</div>;
     }
        
     export default UserItem;
    

    ์ด์— ๋”ฐ๋ผ suspender์—์„œ promise๋ฅผ ์‹คํ–‰ํ•œ ๋’ค์— .then ํ˜ธ์ถœํ•˜๋„๋ก ๋ณ€๊ฒฝ

     // createPromiseResource
     const suspender = promise().then(...) // ๊ธฐ์กด: promise.then(...)
    
  2. ๋ฌธ๋ฒ• ์ˆ˜์ •

     // createPromiseResource
     let suspender = promise()
         .then((res) => {
           status = "fulfilled";
           result = res;
         })
         .catch((err) => {
           status = "rejected";
           result = err;
         });
    
  3. ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฆฌ์•กํŠธ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๋ฒ—์–ด๋‚œ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค. (useRef ๋“ฑ๋„ ์†Œ์šฉ ์—†์Œ - suspense์— ๊ฐ–ํžŒ ์ƒํƒœ์—์„œ๋Š” ์• ์ดˆ์— ๋ Œ๋”๋ง ์กฐ์ฐจ ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋น„๊ตํ•  ๋Œ€์ƒ์ด ์—†์Œ)

    1. store ์ „์—ญ ๊ฐ์ฒด ์ถ”๊ฐ€
    2. ์š”์ฒญ์„ ํ•  ๋•Œ๋งˆ๋‹ค ์–ด๋–ค ์š”์ฒญ์— ๋Œ€ํ•œ ์ƒํƒœ์ธ์ง€ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด key ์ถ”๊ฐ€ (feat. react-query)
     const store = {};
        
     const createPromiseResource = (key, promise) => {
       if (!store[key]) {
         store[key] = {
           status: "pending",
           result: null,
         };
       }
        
       let suspender = promise()
         .then((res) => {
           store[key].status = "fulfilled";
           store[key].result = res;
         })
         .catch((err) => {
           store[key].status = "rejected";
           store[key].result = err;
         });
        
       return {
         read() {
           switch (store[key].status) {
             case "pending":
               throw suspender;
             case "rejected":
               throw store[key].result;
             default:
               return store[key].result;
           }
         },
       };
     };
        
     export default createPromiseResource;
    

๋ฆฌํŒฉํ† ๋ง ํ•˜๋Š” ๊ณผ์ •๊ณผ ์–ป๊ฒŒ ๋œ ๊ฒฐ๊ณผ๋ฌผ์— ๋Œ€ํ•œ ์ •๋ฆฌ๋Š” ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ..!




# ์นดํ…Œ๊ณ ๋ฆฌ