리액스 Suspense 란?
비동기 작업을 보다 효과적으로 관리하여 사용자 경험을 개선하는 기능을 가지고 있다. 서스펜스를 이용하면 컴포넌트에서 필요한 데이터가 준비될 때까지 렌더링을 지연(suspense)시킬 수 있다. 리액트 16.6 버전에서 처음 도입되었다.
주로 아래 두 기능을 위해 사용되지만 이미지, 스크립트 또는 다른 비동기 작업을 위해서도 사용될 수 있다.
1. 코드 분할(Code split)
2. 서버 API 호출과 같은 데이터 가져오기(Data fetching)
코드 분할 시 Suspense 사용 예
import React, {suspense, lazy} from 'react';
const LazyComponent = lazy(() => import ('./lazy/component');
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
<LazyComponent> 는 처음 번들 파일에 포함되지 않고, 요청이 있을 때 별도로 불러오게 된다. LazyComponent 컴포넌트가 로딩 중일 때 서스펜스의 fallback 프로퍼티가 보이게 되고, 컴포넌트가 성공적으로 로드되면, 서스펜스는 지연을 멈추고 해당 컴포넌트를 렌더링하게 된다.
이렇게 서스펜스와 코드 분할을 함께 사용함으로써, 큰 사이즈의 프론트엔드 애플리케이션도 사용자에게 빠르게 첫 화면을 보여줄 수 있으며, 필요한 리소스만 로드하여 성능을 최적화할 수 있게 된다.
Data fetching 시 Suspense 사용 예
아래는 서버 API 호출시 서스펜스가 여러 번 사용되었을 때 어떻게 사용되는지를 보여주고 있다.
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
<ProfileDetails>, <ProfileTimeline /> 컴포넌트 안에서는 서버 API 통신을 통해 데이터를 가져오고 있고, 그에 따른 로딩시 UI 처리를 각각의 <Suspense> 의 fallback 프로퍼티를 통해 보여주고 있다.
다음은 <ProfilePage> 가 렌더링 될 때 일어나는 일이다.
1. 리액트는 <ProfilePage> 를 렌더링하려고 하고, <ProfileDetails>, <ProfileTimeline> 을 children으로 리턴한다.
2. 리액트는 <ProfileDetails> 를 렌더링 시도하지만, 그 안에서 API 호출이 일어나고 있고, 아무 데이터도 가져오지 않은 상태이다. 그러므로 이 컴포넌트는 지연된다. 리액트는 이를 건너 뛰고, 렌더 트리 안의 다른 컴포넌트 먼저 렌더링하려고 한다.
3. 리액트는 <ProfileTimeline> 을 렌더링 시도하지만, 마찬가지고 API 호출이 일어나고 있고, 아무 데이터도 없다. 그러므로 이 컴포넌트 역시 지연된다. 이를 다시 건너 뛰고, 리액트는 다른 컴포넌트를 렌더링하려고 한다.
4. 렌더링할 것이 더 이상 없다. <ProfileDetails> 컴포넌트가 지연됐기에, 리액트는 가장 가까운 상위 서스펜스의 fallback 프로퍼티인 다음을 보여준다: <h1>Loading profile...</h1>
5. 여기서,
<ProfileDetails> 내부의 api 호출이 먼저 마무리되면, 상위 서스펜스의 fallback이 사라지고 <ProfileTimeline > 상위의 서스펜스, Loading posts... 가 보이게된다.
또는, <ProfileTimeline> 내부의 api 호출이 먼저 끝난다면, <ProfileDetails> 의 호출이 아직 완료되지 않은 상태이므로, 가장 바깥쪽의 서스펜스가 보이게 되고, 이 컴포넌트의 api 호출이 끝나면, <ProfileDetails> 컴포넌트가 렌더링되고, 안쪽 컴포넌트도 바로 렌더링된다.
이를 통해 서스펜스는 계층적으로 처리된다는 것을 알 수 있고, 이러한 방식으로 비동기 데이터 로딩을 더 사용자 친화적으로 만들어 줄 수 있다.
Suspense 의 사용 작동원리
그렇다면 서스펜스는 fallback을 보여줄지, children을 보여줄지 어떻게 아는 것일까? 바로 Promise 가 throw 될 때 서스펜스는 fallback 을 보여준다. 즉, 어찌보면 서스펜스는 리액트의 <ErrorBoundary> 와 비슷하게 동작한다고 할 수 있다. 서스펜스는 Promise 가 throw 된 것을 catch한다.
서스펜스를 사용할 컴포넌트는 비동기 상황에서 Promise 를 다뤄야 하며, Suspense를 위해 Promise를 throw 해야 한다.
Promise 가 pending 상태라면 → throw promise
Promise 가 success로 성공한 상태라면 return '데이타'
Promise 가 error 상태라면 throw error
아래는 throw Promise 될 때의 대략적인 구현을 공식문서에서 가져온 것이다.
function fetchUser() {
return new Promise(resolve => {
setTimeout(() => {
resolte({name: 'david' })
}, 1000);
};
}
function wrapPromise(promise) {
let status = 'pending';
let result;
let suspenser = promise.then(
(resolve) => {
status = 'success';
result = r;
},
(reject) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspenser;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
}
};
}
// 사용시
function fetchProfile() {
let userPromise = fetchUser();
return wrapPromise(userPromise);
}
작동 중인 Promise 를 직접 던지는 것이 아니라, Promise 객체의 레퍼런스(참조)를 던지고 Suspense 가 처리할 수 있도록 설계되어 있는 듯하다. 실제 Promise 는 계속 작동하게 되고, 데이터를 성공적으로 가져오면 상태를 업데이트한다.
왜 'Suspense' 를 사용하나요?
- 서스펜스를 사용하면 로딩시 보여줄 상태를 보다 선언적으로 보여줄 수 있다.
이는 개발자가 데이터 로딩과 같은 UI의 동적인 부분들을 좀 더 직관적으로 설계할 수 있도록 도와준다.
- 컴포넌트에서 로딩 시의 UI를 분리하여 컴포넌트 목적에 부합하는 일만 시킬 수 있게 한다.
예를 들어, 컴포넌트에서 데이터 로딩시 아래와 같이 if 문을 이용하여 '로딩중' 상태를 체크하는 보일러플레이트를 없앨 수 있다.
function UserProfile() {
const { data, isLoading } = fetchUser();
...
if (isLoading) return ...
...
}
직접적으로 필요한 일 이외에 부가적인 로직을 없애면서 각 컴포넌트가 로딩 상태를 직접 관리할 필요가 없게 되면서, 컴포넌트 사이에서 일관된 로딩 경험을 제공하고, 중복되는 코드를 줄일 수 있다.
- 또한, 로딩 상태에 따른 디자인 변경 또한 세밀하게 설정할 수 있다. 예를 들어 <UserProfile>, <UserTimeline>이 동시에 나타내야 하는 경우, 하나씩 최대한 빨리 나타내야 하는 경우 등을 서스펜스 위치와 갯수를 조정하면서, 서로의 컴포넌트를 침범하지 않고 효과적으로 이뤄질 수 있다.
- 더욱이, throw 된 프로미스에서 에러가 발생시 에러가 throw되기 때문에 서스펜스와 에러 바운더리와 결합되면 오류를 효과적으로 처리할 수 있다.
에러 바운더리는 동기적 에러에 대해서만 잡아낼 수 있지만, 서스펜스와 함께 사용되면, 서스펜스 내부에서 발생한 에러도 잡을 수 있게 된다. 서스펜스가 자식 컴포넌트로부터 throw된 프로미스를 잡아서 대기하는 동안, 프로미스 내부에서 발생할 수 있는 데이터 로드 중 발생하는 에러 등을 에러 바운더리가 캐치하고 관리할 수 있게 해주기 때문이다.
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</ErrorBoundary>
</Suspense>
);
}
위와 같이 사용된다면 렌더링 에러와 서스펜스 안에서 발생한 데이터 fetching 시의 에러도 처리할 수 있게 된다.
리액트 공식문서 - https://react.dev/reference/react/Suspense#suspense
리액트 공식문서(레거시) - https://17.reactjs.org/docs/concurrent-mode-suspense.html#start-fetching-early
A practical example of Suspense in React 18 - https://dev.to/darkmavis1980/a-practical-example-of-suspense-in-react-18-3lln
React Suspense for Data Fetching: A Comprehensive Guide - https://thecodeframework.com/react-suspense-for-data-fetching-a-comprehensive-guide/
React Suspense: Core Concept, Use Cases, and Best Practices - https://www.angularminds.com/blog/react-suspense-core-concept-use-cases-and-best-practices
How does react Suspense work? - https://www.youtube.com/watch?v=8YQXeqgSSeM
How to Use React.Suspense to wait for an image to load - https://sergiodxa.com/tutorials/react/suspense-image-loading
'React' 카테고리의 다른 글
리액트 상태 관리 전략 - Context API 와 상태관리 라이브러리 (0) | 2024.08.09 |
---|---|
React - useQuery 를 사용할 때 에러가 발생한다면..? (0) | 2024.07.11 |
ErrorBoundary in React || 리액트 에러 바운더리 (0) | 2024.06.17 |
Why React? | 리액트 왜 사용하세요? (0) | 2024.05.14 |