React Suspense 기본 개념
Suspense는 React에서 비동기 작업을 처리할 때, 컴포넌트의 렌더링을 잠시 멈추고, 그 작업이 완료될 때까지 대기하는 기능을 제공해주는 도구이다. 쉽게 말해서, 데이터가 준비될 때까지 기다리게 하고, 그동안 로딩 상태를 보여준다.
주요 개념
Suspense 컴포넌트:
- Suspense 컴포넌트는 비동기 작업이 완료될 때까지 대기 상태를 관리한다.
fallback
속성을 사용해서 비동기 작업이 완료될 때까지 보여줄 UI를 정의할 수 있다.
import React, { Suspense } from 'react'; const MyComponent = React.lazy(() => import('./MyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> ); } export default App;
React.lazy:
- React.lazy를 사용하면 컴포넌트를 동적으로 로드할 수 있다.
- React.lazy는 보통 코드 스플리팅을 위해서 사용된다.
const MyComponent = React.lazy(() => import('./MyComponent'));
API Fetch:
- React Query의 suspense 옵션을 사용하면 간단하게 API가 응답되기 전에 Suspense의 fallback이 실행되도록 만들 수 있다.
응용 해보기
어떻게 동작하는 걸까?
기본적으로 컴포넌트에서 Promise가 Throw되면 Suspense는 fallback의 컴포넌트를 렌더링 한다. 아래는 기본적인 구조를 나타낸 것이지만 기본적으로 리액트에서 컴포넌트는 계속해서 실행되는 개념이기 때문에 Promise가 반복되서 실행되지 않도록 해야한다.
// Don't
function LazyComponent() {
const data = fetchSomething(); // 'data' type is inferred to be Promise.
if (data instanceof Promise) {
throw data;
}
return (
<div>
{data}
</div>
)
}
메모라이즈
기본적으로는 메모리에 저장해두고 같은 키를 바탕으로 같은 객체를 반환하도록 해준다. Promise 타입일 때는 Promise를 Throw 시켜서 Suspense의 fallback을 실행시키는 전략이다.
const cache = new Map<string, unknown>()
function useMemorizedSuspensePromise<T>(keys: string[], fn: () => Promise<T>): T | null {
const key = keys.join('.')
if (!cache.has(key)) {
const promise = fn().then((data) => {
cache.set(key, data);
});
cache.set(key, promise);
}
const cachedData = cache.get(key);
if (cachedData instanceof Promise) {
throw cachedData;
}
return cachedData as T;
}
function LazyComponent() {
const data = useMemorizedSuspensePromise(['fetch', 'something'], () => fetchSomething());
return (
<div>
{data}
</div>
)
}
프로미스 상태 판별
프로미스의 처리 상태를 직관적으로 파악하기 위해서 간단한 함수를 만들어보자.
function createSuspensePromise<T>(promise: Promise<T>) {
let status = 'pending';
let result: T;
const suspender = promise.then(
r => {
status = 'success';
result = r;
},
e => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
}
};
}
위 함수를 이용한 기본적인 사용 방법은 아래와 같다. 컴포넌트에서는 read() 함수만 실행하면 데이터를 가져오거나 Suspense 컴포넌트의 fallback이 실행된다.
const suspensePromise = createSuspensePromise(fetchSomething())
function LazyComponent() {
const data = suspensePromise.read()
return (
<div>
{data}
</div>
)
}
컴포넌트 내부에서 훅처럼 사용하도록 개선해보자.
function useSuspensePromise<T>(promise: Promise<T>, dependents: string[] = []) {
const [resource, setResource] = useState<ReturnType<typeof suspensePromise<T>> | null>(null)
useEffect(() => {
setResource(suspensePromise(promise))
}, dependents)
return resource?.read();
}
function LazyComponent() {
const data = useSuspensePromise(fetchSomething());
return (
<div>
{data}
</div>
)
}
Ghost