์žฅ์šฉ์„ ๋ธ”๋กœ๊ทธ

React Compiler, ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ๊นŒ [1] ์ง€๋‚œ ๊ธ€์—์„œ React Compiler์˜ ์ „์ฒด์ ์ธ ๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ณด์•˜๋‹ค.
์ปดํŒŒ์ผ๋Ÿฌ์˜ ๋™์ž‘ ๋ฐฉ์‹์„ ์‚ดํŽด๋ณด๊ธฐ์ „์—, ๋งŽ์ด ์–ธ๊ธ‰๋˜์—ˆ๋˜ useMemoCache๋ฅผ ๋จผ์ € ์‚ดํŽด๋ณด๊ณ  ๊ฐ€์ž.

useMemoCache implementation by josephsavona ยท Pull Request #25143 ยท facebook/react ยท GitHub

Summary context: I'm still a noob to this codebase so this started as a hacky implementation to get feedback. Initial implementation of useMemoCache that aims to at least get the basics correct, ev...

https://github.com/facebook/react/pull/25143
useMemoCache implementation by josephsavona ยท Pull Request #25143 ยท facebook/react ยท GitHub

useMemoCache์— ๋Œ€ํ•œ ์ดˆ๊ธฐ ๊ตฌํ˜„ PR์ด๋‹ค. ๊ถ๊ธˆํ•˜๋‹ค๋ฉด ํ•œ๋ฒˆ ์‚ดํŽด๋ณด์ž.

์ผ๋‹จ ์ „์ฒด ์ฝ”๋“œ๋ฅผ ์ญ‰ ํ›‘์–ด ๋ณด๊ณ  ๋ถ€๋ถ„๋ถ€๋ถ„ ์‚ดํŽด๋ณด์ž

https://github.com/facebook/react/blob/ee5c19493086fdeb32057e16d1e3414370242307/packages/react-reconciler/src/ReactFiberHooks.js#L1116

// react-reconciler/src/ReactFiberHooks.js
function useMemoCache(size: number): Array<any> {
  let memoCache = null;
  // Fast-path, load memo cache from wip fiber if already prepared
  let updateQueue: FunctionComponentUpdateQueue | null =
    (currentlyRenderingFiber.updateQueue: any);
  if (updateQueue !== null) {
    memoCache = updateQueue.memoCache;
  }
  // Otherwise clone from the current fiber
  if (memoCache == null) {
    const current: Fiber | null = currentlyRenderingFiber.alternate;
    if (current !== null) {
      const currentUpdateQueue: FunctionComponentUpdateQueue | null =
        (current.updateQueue: any);
      if (currentUpdateQueue !== null) {
        const currentMemoCache: ?MemoCache = currentUpdateQueue.memoCache;
        if (currentMemoCache != null) {
          memoCache = {
            // When enableNoCloningMemoCache is enabled, instead of treating the
            // cache as copy-on-write, like we do with fibers, we share the same
            // cache instance across all render attempts, even if the component
            // is interrupted before it commits.
            //
            // If an update is interrupted, either because it suspended or
            // because of another update, we can reuse the memoized computations
            // from the previous attempt. We can do this because the React
            // Compiler performs atomic writes to the memo cache, i.e. it will
            // not record the inputs to a memoization without also recording its
            // output.
            //
            // This gives us a form of "resuming" within components and hooks.
            //
            // This only works when updating a component that already mounted.
            // It has no impact during initial render, because the memo cache is
            // stored on the fiber, and since we have not implemented resuming
            // for fibers, it's always a fresh memo cache, anyway.
            //
            // However, this alone is pretty useful โ€” it happens whenever you
            // update the UI with fresh data after a mutation/action, which is
            // extremely common in a Suspense-driven (e.g. RSC or Relay) app.
            data: enableNoCloningMemoCache
              ? currentMemoCache.data
              : // Clone the memo cache before each render (copy-on-write)
                currentMemoCache.data.map(array => array.slice()),
            index: 0,
          };
        }
      }
    }
  }
  // Finally fall back to allocating a fresh instance of the cache
  if (memoCache == null) {
    memoCache = {
      data: [],
      index: 0,
    };
  }
  if (updateQueue === null) {
    updateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = updateQueue;
  }
  updateQueue.memoCache = memoCache;

  let data = memoCache.data[memoCache.index];
  if (data === undefined) {
    data = memoCache.data[memoCache.index] = new Array(size);
    for (let i = 0; i < size; i++) {
      data[i] = REACT_MEMO_CACHE_SENTINEL;
    }
  } else if (data.length !== size) {
    // TODO: consider warning or throwing here
    if (__DEV__) {
      console.error(
        'Expected a constant size argument for each invocation of useMemoCache. ' +
          'The previous cache was allocated with size %s but size %s was requested.',
        data.length,
        size,
      );
    }
  }
  memoCache.index++;
  return data;
}

์ด์ œ ํ•œ๋ฒˆ ์ˆœ์„œ๋Œ€๋กœ ํ›…์„ ์‚ดํŽด๋ณด์ž.

๋น ๋ฅธ ๊ฒฝ๋กœ

์ฒ˜์Œ ์ด๋ค„์ง€๋Š” ์ž‘์—…์€ ํ˜„์žฌ ๋ Œ๋”๋ง ์ค‘์ธ ํŒŒ์ด๋ฒ„์˜ updateQueue์—์„œ memoCache๋ฅผ ์ฐพ์•„์˜ค๋Š” ์ž‘์—…์ด๋‹ค.
์ด์ „์— ํ˜ธ์ถœ๋œ ์ (๋ Œ๋”๋ง๋œ ์ )์ด ์žˆ๋‹ค๋ฉด ์กด์žฌํ•  ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ถˆํ•„์š”ํ•œ ์บ์‹œ ํ• ๋‹น์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

let memoCache = null;
// Fast-path, load memo cache from wip fiber if already prepared
let updateQueue: FunctionComponentUpdateQueue | null =
  (currentlyRenderingFiber.updateQueue: any);
if (updateQueue !== null) {
  memoCache = updateQueue.memoCache;
}

์บ์‹œ๊ฐ€ ์—†๋‹ค๋ฉด? ๋Œ€์ฒด ํŒŒ์ด๋ฒ„ (Alternate fiber)์—์„œ ์บ์‹œ ๋ณต์ œ

ํ˜„์žฌ ๋ Œ๋”๋ง ์ค‘์ธ fiber์˜ updateQueue์— memoCache๊ฐ€ ์—†๋‹ค๋ฉด, ๋Œ€์ฒด ํŒŒ์ด๋ฒ„(Alternate fiber)๋ฅผ ํ™•์ธํ•œ๋‹ค.

React์˜ Fiber ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” ๊ฐ๊ฐ์˜ Fiber ๋…ธ๋“œ๊ฐ€ โ€˜currentโ€™ ํŠธ๋ฆฌ์™€ โ€˜workInProgressโ€™ ํŠธ๋ฆฌ ์ค‘ ํ•˜๋‚˜์— ์†ํ•˜๊ฒŒ ๋œ๋‹ค.
โ€˜currentโ€™ ํŠธ๋ฆฌ๋Š” ํ˜„์žฌ ํ™”๋ฉด์— ๋ Œ๋”๋ง๋˜์–ด ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๊ณ , โ€˜workInProgressโ€™ ํŠธ๋ฆฌ๋Š” React๊ฐ€ ์—…๋ฐ์ดํŠธ๋ฅผ ์ ์šฉํ•˜๋ ค๊ณ  ํ•˜๋Š” ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.
์—ฌ๊ธฐ์„œ ๋งํ•˜๋Š” ๋Œ€์ฒด ํŒŒ์ด๋ฒ„(Alternate Fiber)๋Š” currentlyRenderingFiber, ์ฆ‰ workInProgress ํŠธ๋ฆฌ์˜ alternate ์ด๋ฏ€๋กœ, current ํŠธ๋ฆฌ์˜ Fiber๋ฅผ ๊ฐ€๋ฆฌํ‚จ๋‹ค.\

์ด ๋Œ€์ฒด ํŒŒ์ด๋ฒ„(current)์—์„œ ์บ์‹œ๋ฅผ ๋ณต์ œํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค.

๋กœ์ง์˜ ํ๋ฆ„์„ ๋ณด๊ธฐ ์œ„ํ•ด ์กฐ๊ฑด๋ฌธ์„ ์ž ์‹œ ์ œ๊ฑฐ ํ•ด๋‘์—ˆ๋‹ค. ์›๋ฌธ์€ ์œ„์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

let memoCache = null;
// ...
// ์บ์‹œ๊ฐ€ ์—†๋‹ค๋ฉด, ๋Œ€์ฒด ํŒŒ์ด๋ฒ„์—์„œ ์บ์‹œ ๋ณต์ œ
if (memoCache == null) {

  const current = currentlyRenderingFiber.alternate; // ๋Œ€์ฒด ํŒŒ์ด๋ฒ„(current)๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  const currentUpdateQueue = current.updateQueue;    // ๋Œ€์ฒด ํŒŒ์ด๋ฒ„์˜ updateQueue๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  const currentMemoCache: ?MemoCache = currentUpdateQueue.memoCache; // ๋Œ€์ฒด ํŒŒ์ด๋ฒ„์˜ memoCache๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

  memoCache = {
    data: enableNoCloningMemoCache
      ? currentMemoCache.data
      : // Clone the memo cache before each render (copy-on-write)
        currentMemoCache.data.map(array => array.slice()),
    index: 0,
  };
}

์ด ๋•Œ ์บ์‹œ๋ฅผ ๋ณต์‚ฌํ•˜๋Š” ๊ณผ์ •์—์„œ enableNoCloningMemoCache ์˜ต์…˜์— ๋”ฐ๋ผ ๋™์ž‘์ด ๋‹ฌ๋ผ์ง„๋‹ค.
enableNoCloningMemoCache ํ”Œ๋ž˜๊ทธ์— ๋”ฐ๋ผ ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ• ์ง€, ์–•์€ ๋ณต์‚ฌ๋ฅผ ํ• ์ง€ ๊ฒฐ์ •ํ•œ๋‹ค.

  • ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‹ค๋ฉด, ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋Š” ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ค„์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์บ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋ฆฌ์Šคํฌ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.
  • ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‹ค๋ฉด, ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์–•์€ ๋ณต์‚ฌํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋Š” ์ด์ „ ๋ Œ๋”๋ง์˜ ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ์ง€๋งŒ, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ์ฆ๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

[Experiment] Reuse memo cache after interruption by acdlite ยท Pull Request #28878 ยท facebook/react ยท GitHub

Adds an experimental feature flag to the implementation of useMemoCache, the internal cache used by the React Compiler (Forget). When enabled, instead of treating the cache as copy-on-write, like w...

https://github.com/facebook/react/pull/28878
[Experiment] Reuse memo cache after interruption by acdlite ยท Pull Request #28878 ยท facebook/react ยท GitHub

์ด๋ถ€๋ถ„์— ๊ธด ์ฃผ์„์ด ๋‹ฌ๋ ค์žˆ๋Š”๋ฐ, ์ด ๋‚ด์šฉ๋„ ์‚ดํŽด๋ณด์ž. ๋ฒˆ์—ญ์„ ๋ฏธ๋ฆฌ ํ•ด๋ณด์•˜๋‹ค.

`enableNoCloningMemoCache`๊ฐ€ ํ™œ์„ฑํ™”๋˜๋ฉด, ํŒŒ์ด๋ฒ„์—์„œ์ฒ˜๋Ÿผ ์บ์‹œ๋ฅผ copy-on-write๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ , 
์‹ฌ์ง€์–ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ปค๋ฐ‹๋˜๊ธฐ ์ „์— ์ค‘๋‹จ๋˜๋”๋ผ๋„ ๋ชจ๋“  ๋ Œ๋”๋ง ์‹œ๋„์—์„œ ๋ชจ๋‘ ๋™์ผํ•œ ์บ์‹œ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ค‘๋‹จ๋˜๋ฉด, ์ผ์‹œ ์ค‘๋‹จ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋“  ๋‹ค๋ฅธ ์—…๋ฐ์ดํŠธ ๋•Œ๋ฌธ์ด๋“ , 
์ด์ „ ์‹œ๋„์—์„œ ๋ฉ”๋ชจ๋œ ๊ณ„์‚ฐ์„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 
React ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋ฉ”๋ชจ ์บ์‹œ์— ๋Œ€ํ•ด ์›์ž์  ์“ฐ๊ธฐ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. 
์ฆ‰, ๋ฉ”๋ชจ์ด์ œ์ด์…˜์˜ ์ถœ๋ ฅ์„ ๊ธฐ๋กํ•˜์ง€ ์•Š๊ณ ๋Š” ์ž…๋ ฅ์„ ๊ธฐ๋กํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ์ปดํฌ๋„ŒํŠธ์™€ ํ›… ๋‚ด์—์„œ ์ผ์ข…์˜ "์žฌ๊ฐœ(resuming)" ํ˜•ํƒœ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ์ด๋ฏธ ๋งˆ์šดํŠธ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ๋งŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”๋ชจ ์บ์‹œ๊ฐ€ ํŒŒ์ด๋ฒ„์— ์ €์žฅ๋˜๊ณ , 
ํŒŒ์ด๋ฒ„์— ๋Œ€ํ•œ ์žฌ๊ฐœ๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์ดˆ๊ธฐ ๋ Œ๋”๋ง ๋™์•ˆ์—๋Š” ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 
์–ด์จŒ๋“  ํ•ญ์ƒ ์ƒˆ๋กœ์šด ๋ฉ”๋ชจ ์บ์‹œ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ด๊ฒƒ๋งŒ์œผ๋กœ๋„ ๊ฝค ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ณ€์ด/์•ก์…˜ ํ›„์— ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋กœ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ๋งˆ๋‹ค ๋ฐœ์ƒํ•˜๋Š”๋ฐ, 
์ด๋Š” Suspense ๊ธฐ๋ฐ˜(์˜ˆ: RSC ๋˜๋Š” Relay)์˜ ์•ฑ์—์„œ ๋งค์šฐ ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค.

React ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋ฉ”๋ชจ ์บ์‹œ์— ๋Œ€ํ•ด ์›์ž์  ์“ฐ๊ธฐ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๋ฉ”๋ชจ์ด์ œ์ด์…˜์˜ ์ถœ๋ ฅ์„ ๊ธฐ๋กํ•˜์ง€ ์•Š๊ณ ๋Š” ์ž…๋ ฅ์„ ๊ธฐ๋กํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ์˜๋ฏธ๋Š” ๋ญ˜๊นŒ?

๐Ÿคทโ€โ™‚๏ธ ์›์ž๊ฐ€ ์—ฌ๊ธฐ์„œ ์™œ ๋‚˜์™€?

์šฐ์„  โ€˜์›์ž์ (Atomic)โ€™ ์ด๋ผ๋Š” ๋‹จ์–ด๋Š” โ€˜๋” ์ด์ƒ ๋‚˜๋ˆŒ ์ˆ˜ ์—†๋Š”โ€™๋ผ๋Š” ์˜๋ฏธ๋กœ ์‚ฌ์šฉ๋œ๋‹ค. (์›์ž๋„ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋Š”๋ฐ์š”? ๋ผ๋Š” ๋ฐœ์–ธ์€ ํ•˜์ง€๋ง์ž)
์ปดํ“จํ„ฐ ๊ณผํ•™์—์„œ์˜ โ€˜์›์ž์  ์ž‘์—…(Atomic operation)โ€˜์€ โ€˜๋” ์ด์ƒ ๋‚˜๋ˆŒ ์ˆ˜ ์—†๋Š” ์ž‘์—…โ€™, ์ฆ‰ ํ•œ ๋ฒˆ์— ์™„์ „ํžˆ ์‹คํ–‰๋˜๊ฑฐ๋‚˜ ์‹คํ–‰๋˜์ง€ ์•Š๋Š” ์ž‘์—…์„ ์˜๋ฏธํ•œ๋‹ค.

์˜ˆ์‹œ๋ฅผ ๋“ค์–ด๋ณด์ž.
์€ํ–‰์—์„œ์˜ ๊ณ„์ขŒ์ด์ฒด๋ฅผ ๋– ์˜ฌ๋ ค๋ณด์ž. ๋งŒ์•ฝ A ๊ณ„์ขŒ์—์„œ B ๊ณ„์ขŒ๋กœ 10๋งŒ์›์„ ์ด์ฒดํ•œ๋‹ค๊ณ  ํ• ๋•Œ ์ด ์ž‘์—…์€ ๋‘๋‹จ๊ณ„๋กœ ์ด๋ค„์ง€๊ฒŒ ๋œ๋‹ค.

  1. A ๊ณ„์ขŒ์—์„œ 10๋งŒ์›์„ ๋นผ๋Š” ์ž‘์—…
  2. B ๊ณ„์ขŒ์— 10๋งŒ์›์„ ๋”ํ•˜๋Š” ์ž‘์—…

๋งŒ์•ฝ ์ด ๊ณผ์ • ์‚ฌ์ด์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ์„œ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ค‘๋‹จ๋œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?
A ๊ณ„์ขŒ์—์„œ๋Š” 10๋งŒ์›์ด ์‚ฌ๋ผ์กŒ์ง€๋งŒ, B ๊ณ„์ขŒ๋Š” ๋ฐ›์ง€ ๋ชปํ•œ ์ƒํƒœ๊ฐ€ ๋˜์–ด๋ฒ„๋ฆฐ๋‹ค. ์ด๋Ÿฐ ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด โ€˜์›์ž์  ์ž‘์—…โ€™์ด ํ•„์š”ํ•˜๋‹ค.
์ฆ‰ ์ด์ฒด ์ž‘์—…์€ ํ•œ๋ฒˆ์— ์™„์ „ํžˆ ์‹คํ–‰๋˜๊ฑฐ๋‚˜ ์‹คํ–‰๋˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค.

React Compiler์˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๊ณผ์ •๋„ ์ด์™€ ๋น„์Šทํ•œ ๊ฐœ๋…์„ ์‚ฌ์šฉํ•œ๋‹ค. ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด์„œ ์„ค๋ช…ํ•ด๋ณด์ž.

์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ปดํŒŒ์ผ ํ•ด๋ณด์ž.

function Component({ active }) {
  let color: string;
  if (active) {
    color = "red";
  } else {
    color = "blue";
  }
  return <div styles={{ color }}>hello world</div>;
}

์ปดํŒŒ์ผ ํ•˜๊ฒŒ๋˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณ€ํ™˜๋œ๋‹ค.

function Component(t0) {
  const $ = _c(2);

  const { active } = t0;
  let color;

  if (active) {
    color = "red";
  } else {
    color = "blue";
  }

  let t1;

  if ($[0] !== color) {
    t1 = (
      <div styles={{ color }}>hello world</div>
    );
    $[0] = color;
    $[1] = t1;
  } else {
    t1 = $[1];
  }

  return t1;
}

์—ฌ๋ฆฌ์„œ ์šฐ๋ฆฌ๊ฐ€ ์ฃผ๋ชฉํ•ด์•ผํ•  ๋ถ€๋ถ„์€ ์ด๋ถ€๋ถ„์ด๋‹ค.

if ($[0] !== color) {
  t1 = (
    <div styles={{ color }}>hello world</div>
  );
  $[0] = color;
  $[1] = t1;
} else {
  t1 = $[1];
}

๋จผ์ € ๊ฐ„๋‹จํžˆ ๋™์ž‘์„ ์„ค๋ช…ํ•ด๋ณด์ž. $๋Š” memoCache๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ์ปดํŒŒ์ผ๋Ÿฌ๋Š” $[0]์—๋Š” ์ด์ „์— ์ €์žฅ๋œ color ๊ฐ’์„ ์ €์žฅํ•˜๊ณ , $[1]์—๋Š” ์ด์ „์— ๋ Œ๋”๋ง๋œ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•˜๊ณ  ์žˆ๋‹ค.
if ($[0] !== color) ์บ์‹œ๋œ color๊ฐ’๊ณผ ํ˜„์žฌ color๊ฐ’์„ ๋น„๊ตํ•œ๋‹ค. ๋งŒ์•ฝ ๋‹ค๋ฅด๋‹ค๋ฉด, ์ƒˆ๋กœ์šด color์— ๋Œ€ํ•œ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.
์ด๋•Œ ์ƒˆ๋กœ์šด ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , $[0]์— ์ƒˆ๋กœ์šด color๊ฐ’์„ ์ €์žฅํ•˜๊ณ , $[1]์— ์ƒˆ๋กœ์šด ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ €์žฅํ•œ๋‹ค.
๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด, ์ด์ „์— ๋ Œ๋”๋ง๋œ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ $[0] = color;์™€ $[1] = t1;์ด ๊ฐ™์€ ๋ธ”๋ก ๋‚ด์—์„œ ์—ฐ์†์ ์œผ๋กœ ์ผ์–ด๋‚œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
์ด๊ฒƒ์ด ๋ฐ”๋กœ โ€˜์›์ž์  ์“ฐ๊ธฐโ€™์ด๋‹ค.

์ด ๋‘ ์ค„์€ ํ•˜๋‚˜์˜ ์›์ž์  ์—ฐ์‚ฐ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ธฐ์—, color ๊ฐ’์ด ์บ์‹œ์— ์“ฐ์ด๋ฉด, ๊ทธ์— ํ•ด๋‹นํ•˜๋Š” ์—˜๋ฆฌ๋จผํŠธ๋„ ๋ฐ˜๋“œ์‹œ ์บ์‹œ์— ์“ฐ์ธ๋‹ค. ์ด๋“ค ์‚ฌ์ด์˜ ์ค‘๊ฐ„ ์ƒํƒœ๋Š” ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค.
์ด์ œ React Compiler์˜ ์›์ž์  ์“ฐ๊ธฐ๊ฐ€ ๋ฌด์—‡์„ ์˜๋ฏธํ•˜๋Š”์ง€ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

๋‹ค์‹œ ๋ณธ๋ž˜ ๋‚ด์šฉ์œผ๋กœ ๋Œ์•„๊ฐ€ ๋ณด์ž.
enableNoCloningMemoCache๊ฐ€ false ๋ผ๋ฉด ๊ฐ ๋ Œ๋”๋ง ์‹œ๋„์‹œ ์ด์ „ ๋ Œ๋”๋ง์˜ ์บ์‹œ๋ฅผ ๋ณต์‚ฌํ•˜๊ณ  ์ด ๋ณต์‚ฌ๋ณธ์„ ์ˆ˜์ •ํ•˜๋Š” ์‹์œผ๋กœ (copy-on-write) ์‚ฌ์šฉํ•œ๋‹ค.
ํ•˜์ง€๋งŒ ์ด ๋ฐฉ๋ฒ•์€ ๋ Œ๋”๋ง์ด ์ค‘๋‹จ๋˜๊ณ  ์žฌ๊ฐœ๋  ๋•Œ๋งˆ๋‹ค ์บ์‹œ๋ฅผ ๋ณต์‚ฌํ•˜๋ฏ€๋กœ, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ์ฆ๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

enableNoCloningMemoCache๊ฐ€ true๋ผ๋ฉด ๋ชจ๋“  ๋ Œ๋”๋ง ์‹œ๋„์—์„œ ๋™์ผํ•œ ์บ์‹œ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ณต์œ ํ•œ๋‹ค.

์ฆ‰, ๋ Œ๋”๋ง์ด ์ค‘๋‹จ(suspended/interrupted)๋˜๊ณ  ์žฌ๊ฐœ ๋˜๋”๋ผ๋„, ์ด์ „ ๋ Œ๋”๋ง ์‹œ๋„์—์„œ์˜ ์บ์‹œ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
์ด๋Š” ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ์— ์žˆ์–ด์„œ ํฐ ์ด์ ์ด๋‹ค.

์ด๋Ÿฐ ๋™์ž‘์ด ๊ฐ€๋Šฅํ•˜๋ ค๋ฉด React ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋ฉ”๋ชจ ์บ์‹œ์— ๋Œ€ํ•ด ์›์ž์  ์“ฐ๊ธฐ๋ฅผ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋ณด์žฅ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๊ฒƒ์ด๋‹ค.

  1. โ€˜Aโ€™๋ Œ๋”๋ง ์‹œ๋„๊ฐ€ color ๊ฐ’์„ โ€˜redโ€™๋กœ ์บ์‹œ์— ์ €์žฅํ•œ๋‹ค.
  2. ๊ทธ๋Ÿฌ๋‚˜ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์บ์‹œ์— ๊ธฐ๋กํ•˜๊ธฐ ์ „์—, ๋ Œ๋”๋ง์ด ์ค‘๋‹จ๋˜์–ด๋ฒ„๋ฆฐ๋‹ค.
  3. โ€˜Bโ€™๋ Œ๋”๋ง ์‹œ๋„๊ฐ€ ์‹œ์ž‘๋˜๋ฉด, ์ด๋•Œ ์บ์‹œ์—๋Š” color์— ๋Œ€ํ•œ ์ž…๋ ฅ์€ ์žˆ์ง€๋งŒ, ์ถœ๋ ฅ์ด ์—†๋Š” ์ƒํƒœ๊ฐ€ ๋œ๋‹ค.

์ด๋Š” ์บ์‹œ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  ์ด๋Š” ๋ Œ๋”๋ง ๊ฒฐ๊ณผ์˜ ๋ถˆ์ผ์น˜๋กœ ์ด์–ด์งˆ ๊ฒƒ์ด๋‹ค.
์ƒํƒœ์— ๋”ฐ๋ฅธ ๋ Œ๋”๋ง ๋ถˆ์ผ์น˜ ๋ฌธ์ œโ€ฆ ์–ด๋””์„œ ๋“ค์–ด๋ณธ์ ์ด ์žˆ์ง€ ์•Š๋‚˜์š”? โ€˜useSyncExternalStoreโ€™๋ฅผ ์„ค๋ช…ํ•  ๋•Œ ๋‚˜์˜ค๋Š” tearing ๋ฌธ์ œ์™€ ๋น„์Šทํ•˜๋‹ค.

What is tearing? ยท reactwg/react-18 ยท Discussion #69 ยท GitHub

What is tearing?

https://github.com/reactwg/react-18/discussions/69
What is tearing? ยท reactwg/react-18 ยท Discussion #69 ยท GitHub

ํ•˜์ง€๋งŒ โ€˜์›์ž์  ์“ฐ๊ธฐโ€™๊ฐ€ ๋ณด์žฅ๋œ๋‹ค๋ฉด, color์— ๋Œ€ํ•œ ์บ์‹œ๊ฐ€ ์žˆ๋‹ค๋Š” ๊ฒƒ์€ ๋ฐ˜๋“œ์‹œ ์—˜๋ฆฌ๋จผํŠธ์— ๋Œ€ํ•œ ์บ์‹œ๋„ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ณด์žฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

ํœด, ๋ญ”๊ฐ€ ๊ฝค๋‚˜ ๋ฉ€๋ฆฌ ์™”๋‹ค. ์ด์ œ ๋‹ค์‹œ useMemoCache๋กœ ๋Œ์•„๊ฐ€๋ณด์ž.

enableNoCloningMemoCache์กฐ๊ฑด์— ๋”ฐ๋ฅธ ๋ถ„๊ธฐ๋ฅผ ๋ณด๋‹ค๊ฐ€ ์—ฌ๊ธฐ๊นŒ์ง€ ์™”๋‹ค. ๊ทธ๋ž˜์„œ enableNoCloningMemoCache๋Š” ์ฐธ์ด๋ƒ ๊ฑฐ์ง“์ด๋ƒ!

// react/shared/ReactFeatureFlags.js

// Test this at Meta before enabling.
export const enableNoCloningMemoCache = false;

ํ˜„ ์‹œ์ ์—์„œ๋Š” enableNoCloningMemoCache๋Š” false๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค.

๋‹ค์‹œ useMemoCache๋กœ ๋Œ์•„๊ฐ€๋ณด์ž.

์บ์‹œ๊ฐ€ ์—†๋‹ค๋ฉด? ์ƒˆ๋กœ์šด ์บ์‹œ ์ƒ์„ฑ

์ด์ œ ๋งˆ์ง€๋ง‰์œผ๋กœ ์บ์‹œ๊ฐ€ ์—†๋‹ค๋ฉด, ์ƒˆ๋กœ์šด ์บ์‹œ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

if (memoCache == null) {
  memoCache = {
    data: [],
    index: 0,
  };
}

์บ์‹œ ํ• ๋‹น

updateQueue๊ฐ€ ์—†๋‹ค๋ฉด ์ƒˆ๋กœ ๋งŒ๋“ค๊ณ , memoCache๋ฅผ ํ• ๋‹นํ•œ๋‹ค.

if (updateQueue === null) {
  updateQueue = createFunctionComponentUpdateQueue();
  currentlyRenderingFiber.updateQueue = updateQueue;
}
updateQueue.memoCache = memoCache;

createFunctionComponentUpdateQueue๋Š” ๊ธฐ๋ณธ์ ์ธ queue๊ฐ์ฒด๋ฅผ return ํ•œ๋‹ค. enableUseMemoCacheHook ํ”Œ๋ž˜๊ทธ์— ๋”ฐ๋ผ ๋‚˜๋‰˜๋Š”๋ฐ ํ˜„์žฌ๋Š” true๋˜์–ด์žˆ๋‹ค.

// NOTE: defining two versions of this function to avoid size impact when this feature is disabled.
// Previously this function was inlined, the additional `memoCache` property makes it not inlined.
// ๋ฉ”๋ชจ : ์ด ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์„ ๋•Œ ํฌ๊ธฐ ์˜ํ–ฅ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ์ด ํ•จ์ˆ˜์˜ ๋‘ ๊ฐ€์ง€ ๋ฒ„์ „์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
// ์ด์ „์— ์ด ํ•จ์ˆ˜๋Š” ์ธ๋ผ์ธ์œผ๋กœ ์ •์˜๋˜์—ˆ์œผ๋ฉฐ, ์ถ”๊ฐ€์ ์ธ `memoCache` ์†์„ฑ์œผ๋กœ ์ธํ•ด ์ธ๋ผ์ธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
let createFunctionComponentUpdateQueue: () => FunctionComponentUpdateQueue;
if (enableUseMemoCacheHook) {
  createFunctionComponentUpdateQueue = () => {
    return {
      lastEffect: null,
      events: null,
      stores: null,
      memoCache: null,
    };
  };
} else {
  createFunctionComponentUpdateQueue = () => {
    return {
      lastEffect: null,
      events: null,
      stores: null,
    };
  };
}

์บ์‹œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜

๋งˆ์ง€๋ง‰์œผ๋กœ ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์บ์ƒˆ๋กœ ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ ,

let data = memoCache.data[memoCache.index]; 

๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋‹ค๋ฉด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ํ• ๋‹นํ•˜๊ณ , REACT_MEMO_CACHE_SENTINEL๋กœ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
REACT_MEMO_CACHE_SENTINEL์€ memoization์ด ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.
ํ˜ธ์ถœ ๋ณ„๋กœ ์บ์‹œ๋Š” Arrayํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— Array๋กœ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.

if (data === undefined) { 
  data = memoCache.data[memoCache.index] = new Array(size);
  for (let i = 0; i < size; i++) {
    data[i] = REACT_MEMO_CACHE_SENTINEL;
  }
}

๋งŒ์•ฝ data๊ฐ€ undefined๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด, ์ฆ‰ ์บ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด, data์˜ ๊ธธ์ด๊ฐ€ ์š”์ฒญ๋œ size์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
ํ™•์ธํ•˜๋Š” ์ด์œ ๋Š”, ์ด์ „ ๋ Œ๋”๋ง์—์„œ ์‚ฌ์šฉ๋œ ์บ์‹œ ๋ฐ์ดํ„ฐ์™€ ํ˜„์žฌ ๋ Œ๋”๋ง์—์„œ ์‚ฌ์šฉํ•  ์บ์‹œ ๋ฐ์ดํ„ฐ์˜ ๊ธธ์ด๊ฐ€ ๋‹ค๋ฅด๋‹ค๋ฉด, ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

else if (data.length !== size) {
  if (__DEV__) {
    console.error(
      'useMemoCache์˜ ๊ฐ ํ˜ธ์ถœ์— ๋Œ€ํ•ด ์ƒ์ˆ˜ ํฌ๊ธฐ ์ธ์ˆ˜๊ฐ€ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.' +
        '์ด์ „ ์บ์‹œ๋Š” ํฌ๊ธฐ %s๋กœ ํ• ๋‹น๋˜์—ˆ์ง€๋งŒ ํฌ๊ธฐ %s๊ฐ€ ์š”์ฒญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
      data.length,
      size,
    );
  }
}

๋งˆ์ง€๋ง‰์œผ๋กœ ์บ์‹œ ์ธ๋ฑ์Šค๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

memoCache.index++;
return data;

์ด์ „ ํŽธ์—์„œ ์‚ดํŽด๋ดค๋˜ โ€˜react-compiler-runtimeโ€™์—์„œ์˜ ๊ตฌํ˜„๊ณผ ๋น„์Šทํ•˜์ง€๋งŒ, ์กฐ๊ธˆ๋” Fiber์— ๋งž๊ฒŒ ์ˆ˜์ •๋œ ๋ถ€๋ถ„์ด ์žˆ๋‹ค.

useMemo์™€ useMemoCache ์ฐจ์ด

์šฐ๋ฆฌ์—๊ฒ ์ต์ˆ™ํ•œ, ๊ทธ๋ฆฌ๊ณ  ์ด์ œ ์žŠํ˜€์ง€๊ฒŒ ๋  useMemo. ์ด๋ฆ„๋„ ๋น„์Šทํ•œ ํƒ“์— ์ฐจ์ด์— ๋Œ€ํ•œ ์˜๋ฌธ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

๊ฐ„๋‹จํ•˜๊ฒŒ useMemo์˜ ๊ตฌํ˜„์„ ๋Œ์•„๋ณด์ž.

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; //
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) { // ์ด์ „ deps์™€ ํ˜„์žฌ deps๊ฐ€ ๊ฐ™๋‹ค๋ฉด ์ด์ „ ๊ฐ’์„ ๋ฐ˜ํ™˜
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate(); // ์ƒˆ๋กœ์šด ๊ฐ’ ์ƒ์„ฑ
  hook.memoizedState = [nextValue, nextDeps]; // ์ƒˆ๋กœ์šด ๊ฐ’๊ณผ deps๋ฅผ ์ €์žฅ
  return nextValue; // ์ƒˆ๋กœ์šด ๊ฐ’ ๋ฐ˜ํ™˜
}

๋‘˜๋‹ค ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์œ„ํ•œ ํ›…์ด์ง€๋งŒ ์กฐ๊ธˆ ๋‹ค๋ฅด๋‹ค, useMemo๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ํ›…์œผ๋กœ, ์˜์กด์„ฑ ๋ฐฐ์—ด์„ ๋ฐ›์•„์„œ ์˜์กด์„ฑ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ ์ƒˆ๋กœ์šด ๊ฐ’์„ ์ƒ์„ฑํ•œ๋‹ค.
useMemo๋Š” ์˜์กด์„ฑ ๋ฐฐ์—ด์„ ํ†ตํ•ด ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์—ฌ๋ถ€๋ฅผ ํ›…์ด ์ฑ…์ž„์ง€๋Š” ๊ฒƒ์ด๋‹ค.
๋ฐ˜๋ฉด useMemoCache๋Š” ์ปดํŒŒ์ผ๋Ÿฌ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ›…์œผ๋กœ, ์˜์กด์„ฑ ๋ฐฐ์—ด์„ ๋ฐ›์ง€ ์•Š๊ณ , ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ฃผ ๋ชฉ์ ์ด๋‹ค.
๊ทธ๋ ‡๊ธฐ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์˜ ์ฑ…์ž„์€ useMemoCache๊ฐ€ ์•„๋‹Œ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋งก๋Š”๋‹ค.

useMemoCache์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

์ด์ œ useMemoCache๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์‹œ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž.
์ข‹์€ ์˜ˆ์‹œ๋“ค์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— ๋งŽ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ๋™์ž‘์„ ์ดํ•ดํ•˜๋Š”๋ฐ ๋„์›€์ด ๋  ๊ฒƒ์ด๋‹ค.

// ๋น„์‹ผ ๊ณ„์‚ฐ์„ ํ•˜๋Š” ํ•จ์ˆ˜
function someExpensiveProcessing(t) { 
  Scheduler.log(`Some expensive processing... [${t}]`);
  return t;
}

// ์„œ์ŠคํŒฌ์Šค๋ฅผ ํŠธ๋ฆฌ๊ฑฐ ํ•˜๊ธฐ์œ„ํ•œ ํ•จ์ˆ˜
function useWithLog(t, msg) { 
  try {
    return React.use(t); 
  } catch (x) {
    Scheduler.log(`Suspend! [${msg}]`);
    throw x;
  }
}

function Data({chunkA, chunkB}) { 
  const a = someExpensiveProcessing(useWithLog(chunkA, 'chunkA')); 
  const b = useWithLog(chunkB, 'chunkB');
  return (
    <>
      {a}
      {b}
    </>
  );
}

function Input() {
  const [input, _setText] = useState('');
  return input;
}

function App({chunkA, chunkB}) {
  return (
    <>
      <div>
        Input: <Input />
      </div>
      <div>
        Data: <Data chunkA={chunkA} chunkB={chunkB} />
      </div>
    </>
  );
}

์ด์™€ ๊ฐ™์€ ํ˜•ํƒœ์˜ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์ปดํŒŒ์ผ ๊ฒฐ๊ณผ๋ฌผ์„ ๊ฐ€์ง€๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์ž.
Data ์ปดํฌ๋„ŒํŠธ๋Š” chunkA์™€ chunkB๋ฅผ ๋ฐ›์•„์™€์„œ, chunkA์— ๋Œ€ํ•œ ๋น„์‹ผ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜๊ณ , chunkB๋Š” ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

// react-reconciler/src/__tests__/useMemoCache-test.js
test('์—…๋ฐ์ดํŠธ ์ค‘์— ์ค‘๋‹จ๋œ(suspended/interrupted) ๋ Œ๋”๋ง ์‹œ๋„์—์„œ์˜ ๊ณ„์‚ฐ์„ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค', async () => { 
  // This test demonstrates the benefit of a shared memo cache. By "shared" I
  // mean multiple concurrent render attempts of the same component/hook use
  // the same cache. (When the feature flag is off, we don't do this โ€” the
  // cache is copy-on-write.)
  // ์ด ํ…Œ์ŠคํŠธ๋Š” ๊ณต์œ  ๋ฉ”๋ชจ ์บ์‹œ์˜ ์ด์ ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. "๊ณต์œ "๋ผ๋Š” ๋ง์€ ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ/ํ›…์˜
  // ์—ฌ๋Ÿฌ ๋™์‹œ ๋ Œ๋”๋ง ์‹œ๋„๊ฐ€ ๋™์ผํ•œ ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. 
  // (๊ธฐ๋Šฅ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๊บผ์ ธ ์žˆ์œผ๋ฉด ์ด๋ ‡๊ฒŒ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค - ์บ์‹œ๋Š” ๋ณต์‚ฌ๋ณธ์ž…๋‹ˆ๋‹ค.)


  function Data(t0) {
    const $ = useMemoCache(5);
    const {chunkA, chunkB} = t0;
    const t1 = useWithLog(chunkA, 'chunkA');
    let t2;

    if ($[0] !== t1) {
      t2 = someExpensiveProcessing(t1);
      $[0] = t1;
      $[1] = t2;
    } else {
      t2 = $[1];
    }

    const a = t2;
    const b = useWithLog(chunkB, 'chunkB');
    let t3;

    if ($[2] !== a || $[3] !== b) {
      t3 = (
        <>
          {a}
          {b}
        </>
      );
      $[2] = a;
      $[3] = b;
      $[4] = t3;
    } else {
      t3 = $[4];
    }

    return t3;
  }

  let setInput;
  function Input() {
    const [input, _set] = useState('');
    setInput = _set;
    return input;
  }

  function App(t0) {
    const $ = useMemoCache(4);
    const {chunkA, chunkB} = t0;
    let t1;

    if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
      t1 = (
        <div>
          Input: <Input />
        </div>
      );
      $[0] = t1;
    } else {
      t1 = $[0];
    }

    let t2;

    if ($[1] !== chunkA || $[2] !== chunkB) {
      t2 = (
        <>
          {t1}
          <div>
            Data: <Data chunkA={chunkA} chunkB={chunkB} />
          </div>
        </>
      );
      $[1] = chunkA;
      $[2] = chunkB;
      $[3] = t2;
    } else {
      t2 = $[3];
    }

    return t2;
  }

  // Resolved ํ”„๋กœ๋ฏธ์Šค ์ƒ์„ฑ
  function createInstrumentedResolvedPromise(value) {
    return {
      then() {},
      status: 'fulfilled',
      value,
    };
  }

  // Pending ํ”„๋กœ๋ฏธ์Šค ์ƒ์„ฑ
  function createDeferred() { 
    let resolve;  
    const p = new Promise(res => { 
      resolve = res;
    });
    p.resolve = resolve;
    return p;  
  }

์ปดํŒŒ์ผ๋œ ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด์ œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์ž.

์ฒซ ๋ฒˆ์งธ ๋ Œ๋”๋ง

์ตœ์ดˆ ๋ Œ๋”๋ง์—์„œ๋Š” chunkA์™€ chunkB๋ฅผ ๋ฐ›์•„์™€์„œ Data ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•œ๋‹ค.
createInstrumentedResolvedPromise๋Š” Resolved๋œ ํ”„๋กœ๋ฏธ์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ฆ‰ ์ด๋ฏธ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
์ด๋•Œ๋Š” A1์— ๊ฑธ๋ ค์žˆ๋Š” expensive process์— ๋Œ€ํ•œ ๋กœ๊ทธ๊ฐ€ ์ฐํžˆ๊ณ , Data ์ปดํฌ๋„ŒํŠธ๋Š” A1B1์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

// Initial render. We pass the data in as two separate "chunks" to simulate a stream (e.g. RSC).
// ์ตœ์ดˆ ๋ Œ๋”๋ง. ๋ฐ์ดํ„ฐ๋ฅผ ๋‘ ๊ฐœ์˜ ๋ณ„๋„ "์ฒญํฌ"๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ŠคํŠธ๋ฆผ(์˜ˆ: RSC)์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค.
const root = ReactNoop.createRoot();
const initialChunkA = createInstrumentedResolvedPromise('A1'); // A1๋กœ resolve๋œ ํ”„๋กœ๋ฏธ์Šค ์ƒ์„ฑ
const initialChunkB = createInstrumentedResolvedPromise('B1'); // B1๋กœ resolve๋œ ํ”„๋กœ๋ฏธ์Šค ์ƒ์„ฑ
await act(() => 
  root.render(<App chunkA={initialChunkA} chunkB={initialChunkB} />), // ์ดˆ๊ธฐ ๋ Œ๋”๋ง
);
assertLog(['Some expensive processing... [A1]']); // 
expect(root).toMatchRenderedOutput(
  <>
    <div>Input: </div>
    <div>Data: A1B1</div>
  </>,
);

์ „ํ™˜์ค‘์— UI ์—…๋ฐ์ดํŠธ

const updatedChunkA = createDeferred(); 
const updatedChunkB = createDeferred(); 

createDeferred ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ updatedChunkA์™€ updatedChunkB๋ผ๋Š” ๋‘ ๊ฐœ์˜ Promise ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ด ๊ฐ์ฒด๋“ค์€ ๋‚˜์ค‘์— ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค

await act(() => {
  React.startTransition(() => {
    root.render(<App chunkA={updatedChunkA} chunkB={updatedChunkB} />);
  });
});

React.startTransition์„ ์‚ฌ์šฉํ•˜์—ฌ UI ์—…๋ฐ์ดํŠธ๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค. transition์€ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋กœ ์Šค์ผ€์ค„๋ง๋˜์–ด, ๋‹ค๋ฅธ ์ž‘์—…์ด ๋๋‚œ ํ›„์— ์‹คํ–‰๋œ๋‹ค.
์ด๋•Œ App ์ปดํฌ๋„ŒํŠธ์— updatedChunkA์™€ updatedChunkB๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ๋ Œ๋”๋งํ•œ๋‹ค.

assertLog(['Suspend! [chunkA]']); 
  const t1 = useWithLog(chunkA, 'chunkA');

useWithLog ํ•จ์ˆ˜์—์„œ chunkA์— ๋Œ€ํ•œ useWithLog๊ฐ€ ์‹คํ–‰๋˜๊ณ , pending ์ค‘์— ์žˆ์Œ์œผ๋กœ use์— ์˜ํ•ด์„œ Suspense๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ ๋˜๊ณ  โ€˜Suspend! [chunkA]โ€˜๊ฐ€ ์ฐํžˆ๊ฒŒ ๋œ๋‹ค.

await act(() => updatedChunkA.resolve('A2'));

updatedChunkA๋ฅผ โ€˜A2โ€™๋กœ resolveํ•œ๋‹ค.

const t1 = useWithLog(chunkA, 'chunkA');
let t2;

if ($[0] !== t1) {
  t2 = someExpensiveProcessing(t1);
  $[0] = t1;
  $[1] = t2;
} else {
  t2 = $[1];
}
const a = t2;
const b = useWithLog(chunkB, 'chunkB');

๋ฐ์ดํ„ฐ๊ฐ€ ์ค€๋น„๋˜์—ˆ์œผ๋ฏ€๋กœ, ๋ Œ๋”๋ง์ด ์žฌ๊ฐœ ๋˜๊ณ , useWithLog๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
์ด๋•Œ t1์€ โ€˜A2โ€™๊ฐ€ ๋˜๊ณ , $[0]์—๋Š” ์ด์ „์— ์ €์žฅ๋œ โ€˜A1โ€™๊ณผ ๋น„๊ตํ•˜์—ฌ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— expensive process๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
๊ทธ์— ๋”ฐ๋ผ โ€˜Some expensive processingโ€ฆ [A2]โ€˜๊ฐ€ ์ฐํžˆ๊ฒŒ ๋œ๋‹ค.

๋ฐ”๋กœ ๋‹ค์Œ์œผ๋กœ useWithLog๊ฐ€ ์‹คํ–‰๋˜๊ณ , chunkB๋Š” ์•„์ง resolve๋˜์ง€ ์•Š์•˜๊ธฐ์— โ€˜Suspend! [chunkB]โ€˜๊ฐ€ ์ฐํžˆ๊ฒŒ ๋œ๋‹ค.
๋‹ค์‹œ ๋ Œ๋”๋ง์ด ์ค‘๋‹จ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ดˆ๊ธฐ UI๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ์ค€๋‹ค.

expect(root).toMatchRenderedOutput(
  <>
    <div>Input: </div>
    <div>Data: A1B1</div>
  </>,
);

์—…๋ฐ์ดํŠธ ์ „ํ™˜ ์ค‘ ๋‹ค๋ฅธ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ

// While waiting for the data to finish loading, update a different part of the screen. This interrupts the refresh transition.
// ๋ฐ์ดํ„ฐ ๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ ํ™”๋ฉด์˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ƒˆ๋กœ ๊ณ ์นจ ์ „ํ™˜์„ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.
//
// In a real app, this might be an input or hover event.
// ์‹ค์ œ ์•ฑ์—์„œ๋Š” ์ž…๋ ฅ ๋˜๋Š” ํ˜ธ๋ฒ„ ์ด๋ฒคํŠธ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
await act(() => setInput('hi!')); // ์ž…๋ ฅ๊ฐ’์„ 'hi!'๋กœ ๋ณ€๊ฒฝ

setInput์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž…๋ ฅ๊ฐ’์„ โ€˜hi!โ€™๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. ์ด ์—…๋ฐ์ดํŠธ๋Š” transition์„ ์ค‘๋‹จ์‹œํ‚ค๊ณ , ์ƒˆ๋กœ์šด ์—…๋ฐ์ดํŠธ๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค.

// Once the input has updated, we go back to rendering the transition.
// ์ž…๋ ฅ์ด ์—…๋ฐ์ดํŠธ๋˜๋ฉด ์ „ํ™˜ ๋ Œ๋”๋ง์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.
if (gate(flags => flags.enableNoCloningMemoCache)) { 
  // We did not have process the first chunk again. We reused the computation from the earlier attempt.
  // ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ „ ์‹œ๋„์—์„œ ๊ณ„์‚ฐ์„ ์žฌ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
  assertLog(['Suspend! [chunkB]']);
} else {
  // Because we clone/reset the memo cache after every aborted attempt, we must process the first chunk again.
  // ์ค‘๋‹จ๋œ ์‹œ๋„๋งˆ๋‹ค ๋ฉ”๋ชจ ์บ์‹œ๋ฅผ ๋ณต์ œ/์žฌ์„ค์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
  assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
}

enableNoCloningMemoCache๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‹ค๋ฉด, ์ด์ „์— ๊ณ„์‚ฐ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ณ , chunkB์— ๋Œ€ํ•œ useWithLog๊ฐ€ ์‹คํ–‰๋˜์–ด โ€˜Suspend! [chunkB]โ€˜๊ฐ€ ์ฐํžŒ๋‹ค.
ํ•˜์ง€๋งŒ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‹ค๋ฉด, ์–•์€ ๋ณต์‚ฌ๋ฅผ ํ†ตํ•ด ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์—, 2์ฐจ์› ๋ฐฐ์—ด๋กœ ๊ด€๋ฆฌ๋˜๋Š” memoCache์˜ ๊ฒฝ์šฐ, $[0] !== t1์ด true๊ฐ€ ๋˜์–ด expensive process๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

expect(root).toMatchRenderedOutput(
  <>
    <div>Input: hi!</div>
    <div>Data: A1B1</div>
  </>,
);

์—ฌ์ „ํžˆ chunkB๋Š” resolve๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด์ „ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.

await act(() => updatedChunkB.resolve('B2')); // ์ฒญํฌB๋ฅผ B2๋กœ resolve

updatedChunkB๋ฅผ โ€˜B2โ€™๋กœ resolveํ•œ๋‹ค. ๋‹ค์‹œ ๋ Œ๋”๋ง์ด ์žฌ๊ฐœ๋œ๋‹ค.

if (gate(flags => flags.enableNoCloningMemoCache)) { 
  // We did not have process the first chunk again. We reused the computation from the earlier attempt.
  // ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ „ ์‹œ๋„์—์„œ ๊ณ„์‚ฐ์„ ์žฌ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
  assertLog([]);
} else {
  // Because we clone/reset the memo cache after every aborted attempt, we must process the first chunk again.
  // ์ค‘๋‹จ๋œ ์‹œ๋„๋งˆ๋‹ค ๋ฉ”๋ชจ ์บ์‹œ๋ฅผ ๋ณต์ œ/์žฌ์„ค์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
  //
  // That's three total times we've processed the first chunk, compared to just once when enableNoCloningMemoCache is on.
  // enableNoCloningMemoCache๊ฐ€ ์ผœ์ ธ ์žˆ์„ ๋•Œ ํ•œ ๋ฒˆ์— ๋น„ํ•ด ์„ธ ๋ฒˆ์งธ ์ฒญํฌ๋ฅผ ์ฒ˜๋ฆฌํ•œ ์ด ํšŸ์ˆ˜์ž…๋‹ˆ๋‹ค.
  assertLog(['Some expensive processing... [A2]']);
}
expect(root).toMatchRenderedOutput( // ๋ Œ๋”๋ง ๊ฒฐ๊ณผ ํ™•์ธ
  <>
    <div>Input: hi!</div>
    <div>Data: A2B2</div>
  </>,
);

ํ”Œ๋ž˜๊ทธ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‹ค๋ฉด, ์ด์ „์— ๊ณ„์‚ฐ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ธฐ์— ๋กœ๊ทธ ์—†์ด ๋ Œ๋”๋ง์ด ์™„๋ฃŒ๋œ๋‹ค.
ํ•˜์ง€๋งŒ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‹ค๋ฉด, ์ด์ „์— ๊ณ„์‚ฐ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ์— โ€˜Some expensive processingโ€ฆ [A2]โ€˜๊ฐ€ ์ฐํžˆ๊ฒŒ ๋œ๋‹ค.

๊ทธ๋ ‡๊ฒŒ Data ์ปดํฌ๋„ŒํŠธ๋Š” A2B2๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋œ๋‹ค.

๋น„ํ™œ์„ฑํ™”๋˜์–ด์žˆ์œผ๋ฉด ์ด 3๋ฒˆ์˜ ๋น„์‹ผ ๊ณ„์‚ฐ์ด ์ผ์–ด๋‚˜๊ฒŒ ๋œ๋‹ค.
ํ•˜์ง€๋งŒ ํ™œ์„ฑํ™”๋˜์–ด์žˆ์œผ๋ฉด 1๋ฒˆ์˜ ๊ณ„์‚ฐ์œผ๋กœ ๋๋‚œ๋‹ค. ๊ฝค๋‚˜ ํฐ ์ฐจ์ด์ด๋‹ค.
ํ˜„์žฌ๋Š” false๋กœ ๋˜์–ด์žˆ์ง€๋งŒ, ์ถ”ํ›„ true๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค๋ฉด ํ•œ์ฐจ๋ก€ ๋” ์„ฑ๋Šฅ์ด ์ข‹์•„์งˆ ๊ฒƒ์ด๋‹ค.

์ž ์ด๋ ‡๊ฒŒ ์ด์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ ๊นŒ์ง€ useMemoCache์˜ ๋™์ž‘์„ ํ™•์ธํ•ด๋ณด์•˜๋‹ค.

๋งˆ์น˜๋ฉฐ

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” useMemoCache์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์•˜๋‹ค.
useMemoCache๋Š” ์ปดํŒŒ์ผ๋Ÿฌ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ›…์œผ๋กœ, ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์œ„ํ•œ ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ฃผ ๋ชฉ์ ์ด๋‹ค.
์•„์ง ์‹คํ—˜์ ์ธ ๊ธฐ๋Šฅ์ด์ง€๋งŒ ์บ์‹œ๋ฅผ ๊ณต์œ  ํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ์—ผ๋‘ํ•ด ๋‘๊ณ  ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

์–ด์ œ๋ณด๋‹ค ๋” ์ปดํŒŒ์ผ๋Ÿฌ์— ๋Œ€ํ•œ ์‹œ์•ผ๊ฐ€ ๋„“์–ด์กŒ๋‹ค.
์ด์ œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์–ด๋–ค ๋ฐฉ์‹์„ ํ†ตํ•ด ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์•Œ๊ฒŒ ๋˜์—ˆ์œผ๋‹ˆ, ๋‹ค์Œ์—๋Š” ์‹ค์งˆ์ ์œผ๋กœ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

ํ•ญ์ƒ ๊ธ€์„ ์–ด๋–ป๊ฒŒ ๋งˆ๋ฌด๋ฆฌ ์ง€์–ด์•ผ ํ• ์ง€๊ฐ€ ๊ณ ๋ฏผ์ด๋‹ค.
์ด๋ฒˆ์—๋„ ์ด๋ ‡๊ฒŒ ๊ทธ๋ƒฅ ๋งˆ๋ฌด๋ฆฌ ํ•ด๋ณด๊ฒ ๋‹ค.

์•ˆ๋…•!

RSS ๊ตฌ๋