๋ฆฌ์กํธ์์ ๋น๋๊ธฐ ์์ ์ ์ฒ๋ฆฌํ ๋, ๋ก๋ฉ ์ค์ fallback UI๋ฅผ ๋ณด์ฌ์ฃผ๊ณ , ๋ฐ์ดํฐ๊ฐ ์ค๋น๋๋ฉด ์ปดํฌ๋ํธ๋ฅผ ์๋์ผ๋ก ๋ค์ ๋ ๋๋งํ๋ ๊ธฐ๋ฅ์ด๋ค.
children
: ๋ ๋๋งํ๋ ค๋ ์ค์ UI์ด๋ค. children
์ ๋ ๋๋ง์ด ์ง์ฐ๋๋ฉด, Suspense๋ fallback
UI๋ฅผ ๋์ ๋ ๋๋งํ๋ค.
fallback
: ์ค์ UI๊ฐ ๋ก๋๋๊ธฐ ์ ๊น์ง ๋์ ๋ ๋๋ง ๋๋ fallback UI์ด๋ค. React Node ํ์์ ๋ฌด์์ด๋ fallback UI๋ก ์ฌ์ฉํ ์ ์๋ค.
<Suspense fallback={<div>๋ก๋ฉ ์ค...</div>}>
<LazyComponent />
</Suspense>
Relay๋ Next.js์ ๊ฐ์ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ ๋, ํ๋ ์์ํฌ๊ฐ ์ ๊ณตํ๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ๊ธฐ๋ฅ์ ํตํด Suspense๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
lazy
๋ฅผ ์ฌ์ฉํ์ฌ ์ปดํฌ๋ํธ๋ฅผ ์ง์ฐ ๋ก๋ฉํ ๋ Suspense๊ฐ ํ์ฑํ๋๋ค.
use
ํ
์ ์ฌ์ฉํด์ Promise ๊ฐ์ ์ฝ๋ ๊ฒฝ์ฐ์๋ Suspense๊ฐ ํ์ฑํ๋๋ค.
๐ก
use
ํ ์ด๋?
Promise ๊ฐ์ ์ฝ๊ณ , ํด๋น Promise๊ฐ ํด๊ฒฐ๋ ๋๊น์ง ์ปดํฌ๋ํธ๋ฅผ ๋๊ธฐ ์ํ๋ก ์ ์งํ๋ ๋ฐ ๋์์ ์ค๋ค.
โช ์ด๋ฅผ ํตํด Susepnse์ ํตํฉํ์ฌ ๋ก๋ฉ ์ค์ผ ๋๋ fallback UI๋ฅผ ๋ ๋๋งํ ์ ์๋ค.
์ปดํฌ๋ํธ๊ฐ ์ฒ์์ผ๋ก ๋ง์ดํธ๋๊ธฐ ์ ์ ์ง์ฐ๋ ๋ ๋๋ง์ ํ๋ ๋์ ์ด๋ค ์ํ๋ ์ ์งํ์ง ์๋๋ค. ์ปดํฌ๋ํธ๊ฐ ๋ก๋๋๋ฉด ๋ฆฌ์กํธ๋ ์ผ์ ์ค์ง๋ ํธ๋ฆฌ๋ฅผ ์ฒ์๋ถํฐ ๋ค์ ๋ ๋๋งํ๋ค. ์ด๋ ์ปดํฌ๋ํธ์ ์ํ๊ฐ ํญ์ ์ต์ ์ํ๋ก ์ ์ง๋๋๋ก ํ๋ค.
์ฝํ ์ธ ๊ฐ ์จ๊ฒจ์ ธ์ผ ํ ๊ฒฝ์ฐ, ๋ฆฌ์กํธ๋ ๋ ์ด์์ ํจ๊ณผ๋ฅผ ์ ๋ฆฌํ๋ค. ์ด๋ ์ฝํ ์ธ ๊ฐ ์จ๊ฒจ์ ธ ์๋ ๋์ DOM ๋ ์ด์์์ ์ธก์ ํ๋ ํจ๊ณผ๊ฐ ์๋ํ์ง ์๋๋ก ๋ณด์ฅํ๋ค. ์ฝํ ์ธ ๊ฐ ๋ค์ ๋ณด์ผ ์ค๋น๊ฐ ๋๋ฉด, ๋ฆฌ์กํธ๋ ๋ ์ด์์ ํจ๊ณผ๋ฅผ ๋ค์ ์คํํ๋ค.
Suspense๊ฐ ์ฝํ
์ธ ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ์์ ๋ ๋ ๋ค์ ์ง์ฐ๋๋ฉด startTransition
, useDeferredValue
๋ก ์ธํ ์
๋ฐ์ดํธ๊ฐ ์๋ ์ด์ fallback UI๋ฅผ ๋ค์ ๋ ๋๋งํ๋ค.
Suspense๋ Effect ๋๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ ๋ด๋ถ์์ ๊ฐ์ ธ์ค๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์งํ์ง ์๋๋ค.
๐ก
startTransition
์ด๋?
UI ์ ๋ฐ์ดํธ์ ์ฐ์ ์์๋ฅผ ์กฐ์ ํ์ฌ ์ฌ์ฉ์๊ฐ ์ํธ์์ฉํ ๋ ๋๋ฆฌ๊ฑฐ๋ ๋ฐฉํด๋ฐ์ง ์๋๋ก ํ๋ ๊ธฐ๋ฅ์ด๋ค.
โชstartTransition
ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ์ํ ๋ณ๊ฒฝ์ ๊ฐ์ธ๋ฉด, ๋ฆฌ์กํธ๋ ์ด ์ํ ๋ณ๊ฒฝ์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ค.
์ด๋ก ์ธํด ๊ธด ์์ ์ด ์๋ฃ๋ ๋๊น์ง ์ฌ์ฉ์์๊ฒ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ค.
๐ก
useDeferredValue
์ด๋?
์ฌ์ฉ์์ ์ ๋ ฅ์ ๋ํ ๋ฐ์์ ์ง์ฐ์์ผ, UI์ ๋ถ๋๋ฌ์์ ์ ์งํ๋ ํ ์ด๋ค. ์ด ํ ์ ์ฃผ๋ก ๊ธด ๋ ๋๋ง ์์ ์ด๋ ๋น์ผ ๊ณ์ฐ์ด ํ์ํ ๊ฒฝ์ฐ์ ์ ์ฉํ๋ค.
โช ์ฃผ์ด์ง ๊ฐ์ ์ ๋ฐ์ดํธ๋ฅผ ์ง์ฐ์์ผ ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ๋ ๋์ UI๊ฐ ์ฆ์ ๋ฐ์ํ๋๋ก ํ ์ ์๋ค.
function handleError(root, thrownValue): void {
do { // ์๋ฌ ์ฒ๋ฆฌ ์์
let erroredWork = workInProgress;
try {
// ์๋ฌ ์ฒ๋ฆฌ ์๋
resetContextDependencies();
resetHooksAfterThrow();
ReactCurrentOwner.current = null;
// ์์ธ ๋ฐ์
// ์ด ํจ์๋ throwValue๋ฅผ ์ฌ์ฉํ์ฌ ์์ธ๋ฅผ ๋์ง๋ค.
// ์ฌ๊ธฐ์ throwValue๊ฐ Promise์ธ ๊ฒฝ์ฐ, Suspense๊ฐ ๋ฐ์ํ ์ ์๋ค.
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes
);
completeUnitOfWork(erroredWork);
} catch (yetAnotherThrownValue) {
// throwException์์ ๋ ๋ค๋ฅธ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด yetAnotherThrownValue์ ์ํด ์์ธ๊ฐ ์กํ๋ค.
thrownValue = yetAnotherThrownValue;
// ํ์ฌ ์์
์ด ์ด๋ฏธ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์์
๊ณผ ๊ฐ๋ค๋ฉด, ์ค๋ฅ๋ฅผ ๋ค์ ๊ฒฝ๊ณ๋ก ์ ํ
// ๊ทธ๋ ์ง ์์ผ๋ฉด, erroredWork๋ฅผ ํ์ฌ ์์
์ ์ค์
if (workInProgress === erroredWork && erroredWork !== null) {
erroredWork = erroredWork.return;
workInProgress = erroredWork;
} else {
erroredWork = workInProgress;
}
continue;
}
return;
} while (true);
}
์ฝ๋๊ฐ ๊ธธ์ง๋ง Suspense๊ฐ ๋์ํ๋ ํต์ฌ ๋ถ๋ถ์ throwException
, completeUnitOfWork
์ด ๋ ํจ์๋ฅผ ์ดํด๋ณด๋ฉด ๋๋ค.
1. Incomplete ํ๋๊ทธ ์ค์
sourceFiber.flags |= Incomplete;
2. Thenable ์ฒดํฌ
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
// ์ด ๊ฐ์ฒด๋ ๋น๋๊ธฐ ์์
์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆด ์ ์๋ ์ํ๋ฅผ ๋ํ๋ธ๋ค.
const wakeable: Wakeable = (value: any);
...
} else {
// ์ผ๋ฐ ์๋ฌ
}
value๊ฐ thenable์ธ์ง ํ์ธํ๋ค. ์ฆ Promise์ ๊ฐ์ ๊ฐ์ฒด์ธ์ง ํ์ธํ๋ฉฐ, ๊ทธ๋ ๋ค๋ฉด ํด๋น ์ปดํฌ๋ํธ๋ ์ผ์ ์ค๋จ(suspend)๋๋ค.
wakeable์ Promise๋ก ๊ฐ์ฃผํ ์ ์์ผ๋ฉฐ, ๋ง์ฝ Promise๊ฐ ์๋๋ผ๋ฉด ์ผ๋ฐ์ ์ธ ์ค๋ฅ๋ก ์ฒ๋ฆฌ๋์ด Error Boundary์ ์ํด ์ฒ๋ฆฌ๋๋ค.
3. Susepnse ๊ด๋ จ ์ฒ๋ฆฌ
const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
getNearestSuspenseBoundaryToCapture()
ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ฅ ๊ฐ๊น์ด Suspense ๊ฒฝ๊ณ๋ฅผ ์ฐพ๋๋ค. ์ด ๊ฒฝ๊ณ๋ Error Boundary์ ์ ์ฌํ ์ญํ ์ ํ๋ค.4. Suspense ๊ฒฝ๊ณ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ
if (suspenseBoundary !== null) {
suspenseBoundary.flags &= ~ForceClientRender;
markSuspenseBoundaryShouldCapture(
suspenseBoundary,
returnFiber,
sourceFiber,
root,
rootRenderLanes
);
// ...
}
ForceClientRender
ํ๋๊ทธ๋ฅผ ์ ๊ฑฐํ๊ณ , markSuspenseBoundaryShouldCapture
๋ฅผ ํธ์ถํ์ฌ Suspense๊ฐ fallback์ ๋ ๋๋งํ๋๋ก ์ค๋นํ๋ค.5. Ping ๋ฐ Retry ๋ฆฌ์ค๋ ์ถ๊ฐ
if (suspenseBoundary.mode & ConcurrentMode) {
// Ping Listener ์ถ๊ฐ
attachPingListener(root, wakeable, rootRenderLanes);
}
// Retry Listener ์ถ๊ฐ
attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes);
return;
ConcurrentMode
์์๋ง ping ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ๊ณ attachRetryListener
๋ฅผ ํตํด weakable(promise)์ ๊ด๋ จ๋ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ๋ค.else {
if (!includesSyncLane(rootRenderLanes)) { // ๋น๋๊ธฐ ์
๋ฐ์ดํธ๋ผ๋ฉด
attachPingListener(root, wakeable, rootRenderLanes);
renderDidSuspendDelayIfPossible();
return;
}
const uncaughtSuspenseError = new Error(
"A component suspended while responding to synchronous input. This " +
"will cause the UI to be replaced with a loading indicator. To " +
"fix, updates that suspend should be wrapped " +
"with startTransition."
);
value = uncaughtSuspenseError;
}
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
...
} else {
const next = unwindWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
if (returnFiber !== null) {
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
} else {
workInProgressRootExitStatus = RootDidNotComplete;
workInProgress = null;
return;
}
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
completeUnitOfWork
ํจ์๋ ํ์ฌ ์์
์ ์ํ๋ฅผ ํ์ธํ๊ณ , ์๋ฃ๋์ง ์์๋ค๋ฉด unwindWork
๋ฅผ ํธ์ถํ์ฌ ์์
์ ์ ๋ฆฌํ๋ค.case SuspenseComponent: {
popSuspenseContext(workInProgress);
const flags = workInProgress.flags;
if (flags & ShouldCapture) {
workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
if (
enableProfilerTimer &&
(workInProgress.mode & ProfileMode) !== NoMode
) {
transferActualDuration(workInProgress);
}
return workInProgress;
}
return null;
}
ShouldCapture
ํ๋๊ทธ๊ฐ ์ค์ ๋์ด ์๋ค๋ฉด, ์ด๋ฅผ DidCapture
๋ก ๋ณ๊ฒฝํ๊ณ ํ์ฌ ์์
์ ๋ฐํํ๋ค.ShouldCapture
์์ DidCapture
๋ก์ ์ ํ์ Suspense๊ฐ ์์ธ๊ฐ ๋ฐ์ํ์ ๋ ํด๋น ์์
์ ๋ค์ ๋ ๋๋งํด์ผ ํจ์ ์๋ฏธํ๋ค.1. Suspense์ ์ญํ
Didcapture
๋ผ๋ ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ค ๋ด์ฉ์ ๋ ๋๋งํ ์ง๋ฅผ ๊ฒฐ์ ํ๋ค. ์ด ํ๋๊ทธ์ ๋ฐ๋ผ fallback UI ๋๋ ๊ธฐ๋ณธ ์์ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ค.2. Offscreen ์ปดํฌ๋ํธ
3. ์ฌ์กฐ์ (reconciling) ๊ณผ์ ์์์ ํ๋จ
Didcapture
ํ๋๊ทธ์ ๋ฐ๋ผ Offscreen์ ๊ฑด๋๋ธ์ง๋ฅผ ๊ฒฐ์ ํ๋ค. ์ด๋ก ์ธํด ์ผ๋ถ Fiber๋ฅผ ์จ๊ธฐ๋ ํจ๊ณผ๊ฐ ๋ฐ์ํ๋ค.4. Promise๊ฐ ๋ฐ์ํ์ ๋
ShouldCapture
ํ๋๊ทธ๊ฐ ์ค์ ๋๋ค. Promise๋ ping, retry ๋ฆฌ์ค๋์ ์ฐ๊ฒฐ๋๋ค.5. ๊ฐ์ฅ ๊ฐ๊น์ด Suspense ์๋ฃ ์
ShouldCapture
๊ฐ Didcapture
๋ก ๋ณ๊ฒฝ๋๊ณ , ํ์ฌ ์ํ์์ fallback UI๋ฅผ ๋ ๋๋งํ ์ค๋น๋ฅผ ํ๋ค.6. ์์ ๋ฃจํ
7. Primise๊ฐ ํด๊ฒฐ๋์์ ๋
๐ก Offscreen ์ปดํฌ๋ํธ๋?
๊ธฐ์กด ์์ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ์ fallback UI๊ฐ ๋ ๋๋ง๋๋ ๋์ ํด๋น ์์ ์ปดํฌ๋ํธ๋ฅผ ํ๋ฉด์ ๋ณด์ด์ง ์๊ฒ ํ๊ณ , ๋์์ ๊ทธ ๋ด๋ถ ์ํ๋ฅผ ์ ์งํ๋ค.
1. ๊ฐ์์ฑ ์ํ: Offscreen ์ปดํฌ๋ํธ๋ visible
, hidden
์ด๋ผ๋ ๋ ๊ฐ์ง ์ํ๋ฅผ ๊ฐ์ง๋ค.
2. hidden ์ํ: ๋ฆฌ์กํธ๋ OffscreenLane์์ ์ฌ์กฐ์ ์ ๋ฏธ๋ฃจ๊ณ (bail out) ์ฒซ ๋ฒ์งธ ํจ์ค์์ ์ฒ๋ฆฌ๋ฅผ ๊ฑด๋๋ฐ์ด ๋ฆฌ์์ค๋ฅผ ์ ์ฝํ ์ ์๋ค.
3. visible ์ํ: ์ผ๋ฐ์ ์ธ ๋ฐฉ์์ผ๋ก ์ฌ์กฐ์ ์ด ์งํ๋๋ค.
4. completeWork ๋จ๊ณ: ์ด ๋จ๊ณ์์ ๊ฐ์์ฑ ํ๋๊ทธ๊ฐ ๋ณ๊ฒฝ๋๋ฉด, ํด๋น ์ปดํฌ๋ํธ์ ๊ฐ์์ฑ ์ํ๊ฐ ์ค์ ๋๋ค.
๋ง์ฝ ์ปดํฌ๋ํธ๊ฐ ๋ณด์ด๋ ์ํ๋ผ๋ฉด ๋ฆฌ์กํธ๋ ์ค์ ๋ก ํ๋ฉด์ ๋ณด์ผ DOM ์์๋ฅผ ์ฌ๊ธฐ์ ์ถ๊ฐํ๋ค.
5. ์ปค๋ฐ ๋จ๊ณ: ์จ๊ฒจ์ง DOM ์์๊ฐ ์ด ๋จ๊ณ์์ ์ถ๊ฐ๋๋ฉฐ, ๋ง์ฝ ๊ฐ์์ฑ ํ๋๊ทธ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ ๋ฆฌ์กํธ๋ DOM ๋ ธ๋๋ฅผ ์จ๊ธฐ๊ฑฐ๋ ๋ค์ ํ์ํ๋ค.
How Suspense works internally in Concurrent Mode 1 - Reconciling flow
How Suspense works internally in Concurrent Mode 2 - Offscreen component
Suspense - React ๊ณต์ ๋ฌธ์(v18.3.1)
Suspense ์ค๋ช
ํ