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


๋ฆฌํŒฉํ† ๋ง

์ด์ œ ๋‚ด๋ถ€ ๊ตฌํ˜„์€ ์™„๋ฃŒํ–ˆ์œผ๋‹ˆ, ์ด ์œ ํ‹ธ์„ ์ข€ ๋” ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ›…์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด์ž.

  • Promise์˜ status์™€ result(error), suspender๋ฅผ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด map ์ž๋ฃŒ๊ตฌ์กฐ์— ์ €์žฅ
    • createCustomPromise ๋ฅผ ํ†ตํ•ด ์œ„ ์ •๋ณด๋“ค์„ ๊ตฌ์กฐํ™”
  • useFetch ์—์„œ๋Š” customPromise์˜ ์ƒํƒœ์— ๋”ฐ๋ผ suspender๋ฅผ ๋˜์งˆ์ง€(pending), ์—๋Ÿฌ๋ฅผ ๋˜์งˆ์ง€(rejected), ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ• ์ง€(fulfilled) ๊ฒฐ์ •ํ•œ๋‹ค.
    • ์ด ๋•Œ suspender๋ฅผ ๋˜์ง€๊ฒŒ ๋˜๋ฉด pending์ด Suspense์— ๊ฐ–ํžˆ๊ฒŒ ๋˜๋Š”๋ฐ, ์ด๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด status์˜ ์ดˆ๊นƒ๊ฐ’์„ null๋กœ ์ง€์ •ํ–ˆ๋‹ค.
    • ์ด๋ฅผ ํ†ตํ•ด ์ฒ˜์Œ์—๋Š” suspender๋ฅผ ๋˜์ง€๊ณ , ์ดํ›„์— status๊ฐ€ pending์ด ๊ฐ์ง€๋˜๋Š” ๊ฒฝ์šฐ๋Š” Suspense๊ฐ€ pending์„ ์ œ๋Œ€๋กœ ๊ฐ€๋‘์ง€ ๋ชปํ•œ ์ƒํ™ฉ์œผ๋กœ ํŒ๋‹จํ•˜๊ณ  ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค.
const PROMISE_STATUS = {
  PENDING: "pending",
  SUCCESS: "fulfilled",
  FAIL: "rejected",
};

const promiseStoreMap = new Map();

const createCustomPromise = (asyncFunction) => {
  const customPromise = {
    status: null,
    result: null,
    error: null,
  };

  const suspender = async () => {
    // ๊ธฐ์กด suspender ๋ถ€๋ถ„
    try {
      customPromise.status = PROMISE_STATUS.PENDING;
      customPromise.result = await asyncFunction();
      customPromise.status = PROMISE_STATUS.SUCCESS;
    } catch (err) {
      customPromise.status = PROMISE_STATUS.FAIL;
      customPromise.error = err;
    }
  };

  customPromise.run = suspender;
  return customPromise;
};

function useFetch(key, asyncFunction) {
  if (!promiseStoreMap.has(key)) {
    promiseStoreMap.set(key, createCustomPromise(asyncFunction));
  }

  const customPromise = promiseStoreMap.get(key);
  if (customPromise.status === null) {
    throw customPromise.run(); // suspender
  } else if (customPromise.status === PROMISE_STATUS.FAIL) {
    throw customPromise.error;
  }

  if (customPromise.stauts === PROMISE_STATUS.PENDING) {
    // status์˜ ์ดˆ๊ธฐ ์ƒํƒœ๋Š” null์ด๊ณ  ์ดํ›„์— pending์œผ๋กœ ๋ฐ”๋€ ๋’ค, fulfilled ํ˜น์€ rejected๊ฐ€ ๋œ๋‹ค.
    // pending์ผ ๊ฒฝ์šฐ suspense์— ๊ฐ–ํžˆ๊ฒŒ ๋  ํ…๋ฐ, ์—ฌ๊ธฐ์„œ ๊ฐ์ง€๋  ๊ฒฝ์šฐ๋Š” suspense์˜ ๋™์ž‘์ด ์ž˜๋ชป๋œ ๊ฒƒ
    throw new Error("Suspense Error");
  }

  return { data: customPromise.result };
}

export default useFetch;

QueryKey๋ฅผ ๊ณ ๋„ํ™” ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ?

react-query์™€ ๊ฐ™์ด queryKey๋ฅผ ๋ฐฐ์—ด๋กœ ๋„˜๊ธด๋‹ค๋˜๊ฐ€, ์บ์‹ฑ ๋“ฑ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ์— ๋Œ€ํ•œ ์ƒ๊ฐ์ด ์Šค์ณ ์ง€๋‚˜๊ฐ”๋‹ค.

์šฐ์„  ์ฒซ ๋ฒˆ์งธ๋กœ queryKey๋ฅผ ๋ฐฐ์—ด๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค.

์ด๋ฅผ ์œ„ํ•ด์„œ map.has()์™€ map.get()์„ key ๊ฐ€ ๋ฐฐ์—ด์ผ ๊ฒฝ์šฐ์—๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝ์ด ํ•„์š”ํ–ˆ๋‹ค. lodash์˜ isEqual์„ ํ†ตํ•ด ๋ฐฐ์—ด์ด ๊ฐ™์€ ๋ฐฐ์—ด์ธ์ง€ ๋น„๊ตํ•˜๊ณ  ํ•„์š”ํ•œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

const hasKey = (key) => {
  let isExist = false;
  promiseStoreMap.forEach((value, _key) => {
    if (_isEqual(_key, key)) {
      isExist = true;
    }
  });
  return isExist;
};

const getByKey = (key) => {
  let mapValue = null;
  promiseStoreMap.forEach((value, _key) => {
    if (_isEqual(_key, key)) {
      mapValue = value;
    }
  });
  return mapValue;
};
function UserListAxios() {
  const ids = [1, 1, 3, 4, 5, 6];
  return (
    <div>
      <h3>์œ ์ € ํ”„๋กœํ•„ ๋ฆฌ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.</h3>
      {ids.map((id) => {
        return (
          <div key={id}>
            <AsyncBoundary
              pendingFallback={<div>๋กœ๋”ฉ์ค‘...</div>}
              rejectedFallback={(e) => <div>์—๋Ÿฌ ๋ฐœ์ƒ!!!{e.message}</div>}
            >
              <UserItemAxios id={id} />
            </AsyncBoundary>
            <hr />
          </div>
        );
      })}
    </div>
  );
}

export default UserListAxios;

susepense.mov

์ด ๊ฒฝ์šฐ์— ๋™์ผํ•œ ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ๋„˜๊ฒจ๋ฐ›์„ ๊ฒฝ์šฐ ์•„๋ž˜ ํ™”๋ฉด๊ณผ ๊ฐ™์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด๋ฒ„๋ฆฌ๋Š”๋ฐ, user-error

์šฐ๋ฆฌ๋Š” ๋™์ผํ•œ ์ฟผ๋ฆฌ ํ‚ค๊ฐ€ ๋„˜์–ด์™”์„ ๋•Œ๋Š” ์ด์ „์— ์บ์‹ฑ๋œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋ณ€๊ฒฝํ•ด์ค„๊ฒƒ์ด๋‹ค.

์—๋Ÿฌ๋Š” ์™œ ๋ฐœ์ƒํ• ๊นŒ?

useFetch ํ•˜๋‹จ์— ์ถ”๊ฐ€ํ•œ ๋ฐ”๋กœ ์•„๋ž˜ ๋ถ€๋ถ„์— ๊ฑธ๋ ค์„œ ์—๋Ÿฌ๋ฅผ throw ํ•˜๊ฒŒ ๋œ ๊ฒƒ์ธ๋ฐ, ๋™์ผํ•œ queryKey๋กœ ํ˜ธ์ถœํ–ˆ์„ ๊ฒฝ์šฐ ๋‘ ๋ฒˆ์งธ๋Š” ๋‹น์—ฐํžˆ ์ฒซ ๋ฒˆ์งธ์—์„œ ๋˜์ง„ suspender๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— pending ์ƒํƒœ๊ฐ€ ๋œ๋‹ค.

  if (customPromise.stauts === PROMISE_STATUS.PENDING) {
    // status์˜ ์ดˆ๊ธฐ ์ƒํƒœ๋Š” null์ด๊ณ  ์ดํ›„์— pending์œผ๋กœ ๋ฐ”๋€ ๋’ค, fulfilled ํ˜น์€ rejected๊ฐ€ ๋œ๋‹ค.
    // pending์ผ ๊ฒฝ์šฐ suspense์— ๊ฐ–ํžˆ๊ฒŒ ๋  ํ…๋ฐ, ์—ฌ๊ธฐ์„œ ๊ฐ์ง€๋  ๊ฒฝ์šฐ๋Š” suspense์˜ ๋™์ž‘์ด ์ž˜๋ชป๋œ ๊ฒƒ
    throw new Error("Suspense Error");
  }

๋”ฐ๋ผ์„œ ํ•ด๋‹น ์กฐ๊ฑด๋ฌธ์— ๋“ค์–ด์™”์„ ๋•Œ ์ด๋ฏธ ๋˜์ ธ์ง„ suspender๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ฐ”๋กœ ๊ทธ suspender๋ฅผ ๋˜‘๊ฐ™์ด ๋˜์ง€๋ฉด ๋œ๋‹ค.

์ด ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•˜๋Š” ๋ถ€๋ถ„์€ pending์ผ ๋•Œ throw ํ•˜๋Š” suspender๋Š” ์ด์ „์— ๋˜์ง„ suspender์—ฌ์•ผ ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค. throw customPromise.run()์„ ํ•ด๋ฒ„๋ฆฌ๋ฉด ์ƒˆ๋กœ์šด proimse๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— network ํƒญ์—์„œ ๋ณด๋ฉด ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์š”์ฒญ์ด ํ•œ ๋ฒˆ ๋” ๊ฐ€๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

throw customPromise.run()

throw customPromise.cache

์ˆ˜์ •ํ•œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

if (customPromise.status === null) {
    // ๊ฐ™์€ queryKey ๊ฐ’์œผ๋กœ ๋ถˆ๋ ธ์„ ๊ฒฝ์šฐ, ๊ฐ™์€ promise๋ฅผ ๋ฐ”๋ผ๋ณผ ์ˆ˜ ์žˆ๋„๋ก cache์— ์‹คํ–‰ํ•œ suspender(.run())๋ฅผ ๋‹ด์•„์ค€๋‹ค.
    customPromise.cache = customPromise.run(); // ์ถ”๊ฐ€๋œ ๋ถ€๋ถ„
    throw customPromise.run(); // suspender
  } else if (customPromise.status === PROMISE_STATUS.FAIL) {
    throw customPromise.error;
  }

  if (customPromise.status === PROMISE_STATUS.PENDING) {
    if (customPromise.cache) {
      // ์บ์‹œ๋œ ๊ฐ’์ด ์žˆ์„ ๊ฒฝ์šฐ ํ•ด๋‹น suspender๋ฅผ ๋‹ค์‹œ ๋˜์ง€๊ณ  ์ด์™ธ์˜ ๊ฒฝ์šฐ์—๋Š” ์—๋Ÿฌ๋กœ ํŒ๋‹จ
      throw customPromise.cache; // ์ถ”๊ฐ€๋œ ๋ถ€๋ถ„
    }
    throw new Error("Suspense Error");
  }

๋‹ค๋ฅธ ์ˆ˜์ • ์‚ฌํ•ญ์œผ๋กœ๋Š” ๋ฐฐ์—ด๋กœ ๋ฐ›์€ queryKey๋ฅผ ๊ทธ๋Œ€๋กœ map ์˜ key ๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  JSON.stringify๋ฅผ ํ•ด์„œ string ํƒ€์ž…์˜ key๋กœ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€๊ฒฝํ–ˆ๋‹ค. (๋ฐฐ์—ด์„ key๋กœ ์“ธ ๊ฒฝ์šฐ, set, get, has ๋“ฑ์—์„œ ์›ํ•˜๋Š”๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š์•„ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด ์คฌ์–ด์•ผ ํ–ˆ์Œ)

์™„์„ฑ๋œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค!

const PROMISE_STATUS = {
  PENDING: "pending",
  SUCCESS: "fulfilled",
  FAIL: "rejected",
};

const promiseStoreMap = new Map();
window.promiseMap = promiseStoreMap;

const createCustomPromise = (asyncFunction) => {
  const customPromise = {
    status: null,
    result: null,
    error: null,
    cache: null,
  };

  const suspender = async () => {
    // ๊ธฐ์กด suspender ๋ถ€๋ถ„
    try {
      customPromise.status = PROMISE_STATUS.PENDING;
      customPromise.result = await asyncFunction();
      customPromise.status = PROMISE_STATUS.SUCCESS;
    } catch (err) {
      customPromise.status = PROMISE_STATUS.FAIL;
      customPromise.error = err;
    }
  };

  customPromise.run = suspender;
  return customPromise;
};

function useFetch(key, asyncFunction) {
  const parsedKey = JSON.stringify(key);
  if (!promiseStoreMap.has(parsedKey)) {
    promiseStoreMap.set(parsedKey, createCustomPromise(asyncFunction));
  }

  const customPromise = promiseStoreMap.get(parsedKey);

  if (customPromise.status === null) {
    // ๊ฐ™์€ queryKey ๊ฐ’์œผ๋กœ ๋ถˆ๋ ธ์„ ๊ฒฝ์šฐ, ๊ฐ™์€ promise๋ฅผ ๋ฐ”๋ผ๋ณผ ์ˆ˜ ์žˆ๋„๋ก cache์— ์‹คํ–‰ํ•œ suspender(.run())๋ฅผ ๋‹ด์•„์ค€๋‹ค.
    customPromise.cache = customPromise.run();
    throw customPromise.run(); // suspender
  } else if (customPromise.status === PROMISE_STATUS.FAIL) {
    throw customPromise.error;
  }

  if (customPromise.status === PROMISE_STATUS.PENDING) {
    // status์˜ ์ดˆ๊ธฐ ์ƒํƒœ๋Š” null์ด๊ณ  ์ดํ›„์— pending์œผ๋กœ ๋ฐ”๋€ ๋’ค, fulfilled ํ˜น์€ rejected๊ฐ€ ๋œ๋‹ค.
    // pending์ผ ๊ฒฝ์šฐ suspense์— ๊ฐ–ํžˆ๊ฒŒ ๋  ํ…๋ฐ, ์—ฌ๊ธฐ์„œ ๊ฐ์ง€๋  ๊ฒฝ์šฐ๋Š” suspense์˜ ๋™์ž‘์ด ์ž˜๋ชป๋œ ๊ฒƒ
    if (customPromise.cache) {
      // ์บ์‹œ๋œ ๊ฐ’์ด ์žˆ์„ ๊ฒฝ์šฐ ํ•ด๋‹น suspender๋ฅผ ๋‹ค์‹œ ๋˜์ง€๊ณ  ์ด์™ธ์˜ ๊ฒฝ์šฐ์—๋Š” ์—๋Ÿฌ๋กœ ํŒ๋‹จ
      throw customPromise.cache;
      // throw customPromise.run();
    }
    throw new Error("Suspens Error");
  }

  return { data: customPromise.result };
}

export default useFetch;

๊ฒฐ๋ก 

react-query๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ํŒจ์นญ ์‹œ์—๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ›…์„ ๋งŒ๋“ค์–ด๋ณด๋ฉฐ, Suspense์˜ ๊ธฐ๋ณธ ์›๋ฆฌ์— ๋Œ€ํ•ด์„œ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

ErrorBoundary๊ฐ€ throw ํ•œ Error๋ฅผ ๊ฐ์ง€ํ•˜๋“ฏ Suspense๋Š” throw ํ•œ Promise๋ฅผ ๊ฐ์ง€ํ•œ๋‹ค.

๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ ํŒจ์นญ ์‹œ์— Promise์˜ ์ƒํƒœ์— ๋”ฐ๋ผ response๋ฅผ ๋ฆฌํ„ดํ•˜๊ฑฐ๋‚˜ error๋ฅผ ๋˜์ง€๊ฑฐ๋‚˜, pending์ผ ๊ฒฝ์šฐ promise๋ฅผ ๋˜์งˆ ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒƒ ๋งŒ์œผ๋กœ promise๋ฅผ suspense์— ๊ฐ€๋‘˜ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

โ†’ react-query์˜ suspense ์˜ต์…˜๊ณผ React.Suspense๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์„ ๊ฒƒ ๊ฐ™๋‹ค.

๋˜ํ•œ SSR์„ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด SSRSuspender๋ฅผ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ๋Š” mount ๋˜์ง€ ์•Š์•˜์„ ๋•Œ fallback์„ ๋‚ด๋ ค์ฃผ๋„๋ก ํ•˜๋Š” ๋ถ€๋ถ„๋งŒ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.




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