왜 loading이 아니라 fallback일까# function App () {
return (
// 왜 loading이나 placeholder가 아니라 fallback일까?
< Suspense fallback ={ < div >로딩중...</ div > } >
< DataComponent />
</ Suspense >
);
}
function App () {
return (
// 왜 loading이나 placeholder가 아니라 fallback일까?
< Suspense fallback ={ < div >로딩중...</ div > } >
< DataComponent />
</ Suspense >
);
}
prop 이름이 loading도 placeholder도 아닌 fallback입니다. Suspense가 안에서 어떻게 움직이는지 보면 이 이름이 자연스럽게 읽힙니다.
핵심: 컴포넌트가 Promise를 던진다# JavaScript의 throw는 보통 에러를 던지는 데 쓰지만, 아무 값이나 던질 수 있습니다. Suspense는 이 점을 이용합니다. 데이터가 아직 없는 컴포넌트는 렌더링 도중에 "아직 없다"는 뜻으로 Promise를 던지고, React는 그것을 받아 가장 가까운 <Suspense>의 fallback을 대신 그립니다.
데이터를 읽는 쪽은 세 가지 경우를 구분합니다.
// 개념을 보여 주는 의사 코드
function readData ( key ) {
if (cache. has (key)) {
const entry = cache. get (key);
if (entry instanceof Promise ) throw entry; // 요청 중: 같은 Promise를 다시 던진다
return entry; // 도착함: 값을 돌려준다
}
// 첫 요청: Promise를 캐시에 넣고 던진다
const promise = fetchData (key). then (( value ) => cache. set (key, value));
cache. set (key, promise);
throw promise;
}
// 개념을 보여 주는 의사 코드
function readData ( key ) {
if (cache. has (key)) {
const entry = cache. get (key);
if (entry instanceof Promise ) throw entry; // 요청 중: 같은 Promise를 다시 던진다
return entry; // 도착함: 값을 돌려준다
}
// 첫 요청: Promise를 캐시에 넣고 던진다
const promise = fetchData (key). then (( value ) => cache. set (key, value));
cache. set (key, promise);
throw promise;
}
캐시가 핵심입니다. React는 Promise가 끝나면 컴포넌트를 처음부터 다시 렌더링합니다. 그때 같은 key로 캐시를 읽어 값을 얻어야 합니다. 렌더링할 때마다 새 Promise를 만들어 던지면 끝없이 fallback만 보게 됩니다.
참고로 Promise를 던지는 방식은 React가 문서로 공개한 API가 아닙니다. react-query 같은 라이브러리들이 맞춰 쓰는 약속이고, 그래서 직접 구현하기보다 라이브러리를 거쳐 쓰게 됩니다.
React 쪽에서 일어나는 일# // 개념을 보여 주는 의사 코드
try {
return renderComponent ();
} catch (thrown) {
if (thrown instanceof Promise ) {
thrown. then (retryRender); // 끝나면 다시 렌더링하도록 예약하고
return renderNearestSuspenseFallback (); // 가장 가까운 Suspense의 fallback을 그린다
}
throw thrown; // 진짜 에러는 Error Boundary로 보낸다
}
// 개념을 보여 주는 의사 코드
try {
return renderComponent ();
} catch (thrown) {
if (thrown instanceof Promise ) {
thrown. then (retryRender); // 끝나면 다시 렌더링하도록 예약하고
return renderNearestSuspenseFallback (); // 가장 가까운 Suspense의 fallback을 그린다
}
throw thrown; // 진짜 에러는 Error Boundary로 보낸다
}
Error Boundary와 구조가 같습니다. 던져진 것이 에러면 Error Boundary가, Promise면 Suspense가 받습니다. 둘 다 안쪽을 그릴 수 없을 때 대신 그릴 것을 가진 경계입니다.
이 방식으로 얻는 것# 컴포넌트는 데이터가 있다고 가정하고 씁니다. 로딩 상태를 들고 다니지 않습니다. 이 이야기는 React 로딩처리와 Suspense의 등장 에 썼습니다. 덜 된 화면이 보이지 않습니다. Promise가 던져진 순간 그 아래 트리의 렌더링이 멈추므로, 일부만 채워진 UI가 노출되지 않습니다. 오래된 응답이 최신 화면을 덮어쓰지 않습니다. 응답이 올 때마다 setData를 하는 방식에서는 늦게 도착한 이전 요청의 응답이 화면을 덮어쓸 수 있습니다. 캐시를 key로 읽는 방식에서는 화면이 늘 현재 key의 값만 읽으므로 이 문제가 생기지 않습니다. 그래서 fallback# loading은 상태의 이름이고 fallback은 동작의 이름입니다. Suspense는 로딩 상태를 관리하지 않습니다. 안쪽을 그릴 수 없다는 신호를 받으면 준비해 둔 UI로 물러날(fall back) 뿐입니다. 이름이 동작 방식을 그대로 말하고 있습니다.