Suspense란?
React의 비동기 작업 처리와 Suspense의 등장
React에서는 3가지 방법을 통해 렌더링 시 비동기 작업을 처리할 수 있다.
- fetch on render: 컴포넌트 렌더링을 먼저 시작하고
useEffect
나componentDidMount
로 비동기 처리한다. - fetch then render:
useEffect
나componentDidMount
로 화면을 그리는데 필요한 데이터를 모두 조회한 후 렌더링을 시작한다. - render as you fetch: 비동기 작업과 렌더링을 동시에 시작한다. 즉시 초기 상태를 렌더링(fallback rendering)하고, 비동기 작업이 완료되면 다시 렌더링한다.
fetch on render
나 fetch then render
방식의 단점
fetch on render
나 fetch then render
방식에는 아래와 같은 어려움이 있다.
- Race Condition
- 비동기 작업들이 가지는 자신만의 생명주기로 인해 Race Condition이 발생한다.
- fetching과 렌더링 사이에 동기화를 통해 해결할 수 있다.
if (isPending) { return <div>Loading...</div>; }
- Waterfall Problem
- 동기화 처리로 인한 동시성이 불가능해지고 효율성이 떨어진다.
- High Coupling
- 비동기 작업들을 한 곳에서 동시적으로 처리하고, 그 결과를 컴포넌트로 전달하면 동시성을 보장할 수 있지만 컴포넌트들 간의 역할분담이 불명확해지고 결합도가 높아진다.
useEffect(() => { Promise.all([fetching1, fetching2]).then((res) => { setState(res); }); }, []);
- 비동기 작업들을 한 곳에서 동시적으로 처리하고, 그 결과를 컴포넌트로 전달하면 동시성을 보장할 수 있지만 컴포넌트들 간의 역할분담이 불명확해지고 결합도가 높아진다.
위와 같은 어려움으로 인해 render as you fetch
방식을 사용하는 Suspense가 등장하게 됐다.
Suspense의 동작 방식
Suspense는 useEffect
나 이벤트 핸들러 내부에서 페칭하는 경우를 감지하지 않는다.
Suspense를 동작시키기 위해서는 children
에 속한 컴포넌트가 비동기 작업을 진행할 때 예외 처리로 Promise
를 throw
해줘야 한다.
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
export function fetchProfileData() {
let userPromise = fetchUser();
let postsPromise = fetchPosts();
return {
user: wrapPromise(userPromise),
posts: wrapPromise(postsPromise)
};
}
function wrapPromise(promise) {
let status = 'pending';
let result;
let 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;
}
}
};
}
또한 아래와 같은 방법을 통해 Suspense 컴포넌트를 활성화할 수 있다.
-
Suspense를 도입한 프레임워크를 사용한 데이터 페칭
-
lazy를 사용한 지연 로딩 컴포넌트 코드
import { lazy } from 'react'; // 최상위 레벨에서 선언 const MarkdownPreview = lazy(() => import('./MarkdownPreview.js')); function Editor() { (...) <Suspense fallback={<Loading />}> <h2>Preview</h2> <MarkdownPreview /> </Suspense> (...) }
Suspense의 사용법
Suspense를 사용하면 children
이 로딩을 완료할 때까지 fallback
을 표시할 수 있다.
fallback
은 로딩이 완료되지 않은 경우에 보여주는 대체 UI로 주로 로딩 스피너나 스켈레톤을 사용한다.
Suspense는 children
이 일시 중단되면 자동으로 fallback
으로 전환되고, 데이터가 준비되면 다시 children
으로 전환된다.
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
기본적으로 Suspense 내부의 전체 트리는 단일 단위로 취급된다. 예를 들어, 다음 컴포넌트 중 하나만 데이터 대기를 위해 일시 중단하더라도 모든 컴포넌트가 함께 로딩 표시기로 대체된다.
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
컴포넌트가 일시 중단되면 가장 가까운 상위 Suspense 컴포넌트가 폴백을 표시한다. 이를 통해 여러 Suspense 컴포넌트를 중첩하여 로딩 시퀀스를 만들 수 있다.
<Suspense fallback={<BigSpinner />}>
{' '}
// 1
<Biography /> // 2
<Suspense fallback={<AlbumsGlimmer />}>
{' '}
// 3
<Panel>
<Albums /> // 4
</Panel>
</Suspense>
</Suspense>
갑작스러운 결론
Suspense 사용법을 익히고 캐싱을 직접 구현하기 어려우니 react-query
를 쓰자!
useDeferredValue
훅, useTransition
훅은 어렵다.
참고
https://react-ko.dev/reference/react/Suspense
https://www.daleseo.com/react-suspense/
https://velog.io/@jay/Suspense
https://fe-developers.kakaoent.com/2021/211127-211209-suspense/