Suspense의 내부 동작 방식 이해하기🔗
function App() {
return (
// 왜 loading 이나 placeholder가 아니라 fallback일까?
<Suspense fallback={<div>로딩중...</div>}>
<DataComponent />
</Suspense>
)
}
function App() {
return (
// 왜 loading 이나 placeholder가 아니라 fallback일까?
<Suspense fallback={<div>로딩중...</div>}>
<DataComponent />
</Suspense>
)
}
핵심 원리: Promise Throwing🔗
Suspense의 동작 원리를 이해하기 위한 가장 핵심적인 개념은 "Promise를 throw한다"는 것입니다. 일반적으로 JavaScript에서 throw는 에러를 던지는 데 사용되지만, React는 이를 창의적으로 활용했습니다.
세 가지 상태와 Promise🔗
데이터를 불러오는 상황에서 가능한 세 가지 상태가 있습니다:
- 아직 데이터를 요청하지 않은 상태
- 데이터를 요청 중인 상태
- 데이터 로딩이 완료된 상태
// Suspense pseudo-code
function fetchData() {
if (cache.has(key)) {
const result = cache.get(key);
if (result instanceof Promise) {
// 아직 로딩 중 - Promise를 throw
throw result;
}
// 데이터가 있음 - 반환
return result;
}
// 첫 요청 - Promise 생성 및 throw
const promise = new Promise(/* ... */);
cache.set(key, promise);
throw promise;
}
// Suspense pseudo-code
function fetchData() {
if (cache.has(key)) {
const result = cache.get(key);
if (result instanceof Promise) {
// 아직 로딩 중 - Promise를 throw
throw result;
}
// 데이터가 있음 - 반환
return result;
}
// 첫 요청 - Promise 생성 및 throw
const promise = new Promise(/* ... */);
cache.set(key, promise);
throw promise;
}
React의 내부 처리 흐름🔗
- Promise 감지
// React pseudo-code
try {
const result = renderComponent();
// 정상적으로 렌더링됨
return result;
} catch (thrownValue) {
if (thrownValue instanceof Promise) {
// Suspense 처리
return findAndRenderSuspenseFallback();
}
// 실제 에러는 Error Boundary로 전파
throw thrownValue;
}
// React pseudo-code
try {
const result = renderComponent();
// 정상적으로 렌더링됨
return result;
} catch (thrownValue) {
if (thrownValue instanceof Promise) {
// Suspense 처리
return findAndRenderSuspenseFallback();
}
// 실제 에러는 Error Boundary로 전파
throw thrownValue;
}
- Suspense의 내부 동작
// Suspense pseudo-code
function Suspense({ children, fallback }) {
let content;
try {
content = children;
} catch (promise) {
if (promise instanceof Promise) {
// Promise를 구독하고 resolve될 때 재렌더링
promise.then(() => rerender());
return fallback;
}
throw promise; // 실제 에러는 상위로 전파
}
return content;
}
// Suspense pseudo-code
function Suspense({ children, fallback }) {
let content;
try {
content = children;
} catch (promise) {
if (promise instanceof Promise) {
// Promise를 구독하고 resolve될 때 재렌더링
promise.then(() => rerender());
return fallback;
}
throw promise; // 실제 에러는 상위로 전파
}
return content;
}
Promise Throwing의 장점🔗
- 선언적 코드 작성
- 비동기 상태 처리를 위한 별도의 상태 관리가 필요 없음
- try-catch와 유사한 친숙한 패턴 활용
- 자동 경쟁 상태(Race Condition) 방지
// 기존의 방식
async function fetchData() {
setLoading(true);
try {
const data = await api.fetch();
setData(data); // 이전 요청의 응답이 늦게 도착하면?
} finally {
setLoading(false);
}
}
// Suspense 방식
function fetchData() {
const promise = api.fetch();
throw promise; // React가 자동으로 최신 상태만 반영
}
// 기존의 방식
async function fetchData() {
setLoading(true);
try {
const data = await api.fetch();
setData(data); // 이전 요청의 응답이 늦게 도착하면?
} finally {
setLoading(false);
}
}
// Suspense 방식
function fetchData() {
const promise = api.fetch();
throw promise; // React가 자동으로 최신 상태만 반영
}
- 렌더링 일시 중단
- Promise가 throw되면 해당 컴포넌트 트리의 렌더링이 자동으로 중단
- 불완전한 UI가 사용자에게 노출되는 것을 방지
React의 특별한 처리🔗
React는 일반적인 에러와 thrown Promise를 구분해서 처리합니다.
// React pseudo-code
function handleThrow(thrownValue) {
if (thrownValue instanceof Promise) {
// 1. Promise를 캐시에 저장
// 2. Suspense 경계를 찾아 fallback 렌더링
// 3. Promise가 resolve되면 재렌더링 스케줄링
suspendRenderingAndHandlePromise(thrownValue);
} else {
// 일반적인 에러는 Error Boundary로 전파
throwErrorToErrorBoundary(thrownValue);
}
}
// React pseudo-code
function handleThrow(thrownValue) {
if (thrownValue instanceof Promise) {
// 1. Promise를 캐시에 저장
// 2. Suspense 경계를 찾아 fallback 렌더링
// 3. Promise가 resolve되면 재렌더링 스케줄링
suspendRenderingAndHandlePromise(thrownValue);
} else {
// 일반적인 에러는 Error Boundary로 전파
throwErrorToErrorBoundary(thrownValue);
}
}
마무리: 왜 fallback인가?🔗
이제 처음 질문으로 돌아가봅시다. Suspense가 'loading'이나 'placeholder' 대신 'fallback'이라는 prop 이름을 사용하는 이유가 명확해졌을 것입니다.
이는 단순한 로딩 상태가 아니라, Promise가 throw되었을 때 "fall back"할 대체 UI를 의미합니다. 이 이름은 Suspense의 내부 동작 방식을 정확하게 반영하고 있습니다:
- 컴포넌트가 Promise를 throw함
- React가 이를 캐치하여 처리
- 가장 가까운 Suspense의 fallback UI로 "fall back"함
이러한 방식으로 React는 동기적인 코드처럼 보이는 방식으로 비동기 데이터 로딩을 처리할 수 있게 되었습니다.