2. Suspense
리엑트v16.6에 새롭게 추가된 Suspense 구현체와 그 개념은 React.lazy
를 통해 웹에서 필요한 자원을 동적으로 불러와 필요할 때 로드할 수 있도록 하여 사용자가 첫 페이지를 로드할 때 당장 사용되지 않은 자원을 불러오는 낭비를 막고 전략적인 자원 관리를 할 수 있게 한다.
여기서, React.lazy
를 통해 분리된 코드 분할 구성 요소를 가져올 때 사용자는 약간의 지연을 경험하게 되는데, Suspense는 fallback
에서 로딩 중에 노출시킬 UI를 지정하여 동적으로 불러오는 UI의 선언적인 관리가 가능하도록 한다.
import React, { lazy } from "react";
const AvatarComponent = React.lazy(() => import("./AvatarComponent"));
return (
<Suspense fallback={<Loader />}>
<AvatarComponent />
</Suspense>
);
만약, 로딩 실패에 대해 처리하고 싶다면, Suspense와 Error Boundary 를 함께 사용하여 지연 로딩 중 발생한 에러가 전역으로 퍼지지 않고, 에러 경계 내에서 처리할 수 있도록 하는 에러 처리 또한 선언적으로 관리할 수 있다.
import React, { lazy } from "react";
const AvatarComponent = React.lazy(() => import("./AvatarComponent"));
return (
<ErrorBoundary>
<Suspense fallback={<Loader />}>
<AvatarComponent />
</Suspense>
</ErrorBoundary>
);
Suspense for Data fetching
웹에서 지연 로딩을 통해 동적으로 가져올 수 있는 자원의 대상은 웹에서 필요로 하는 모든 자원이 그 대상이 될 수 있다.
리엑트v16.6에서의 Suspense는 주로 자바스크립트 번들을 분리하고 이를 지연 로딩의 대상으로 삼고 이를 기다리고 선언적으로 관리할 수 있도록 하는 역할이었다면, React18에서의 Suspense는 웹에서 필요로 하는 모든 자원에 대한 기다림을 관리할 수 있는 구현체로 확장된다.
리엑트에서 비동기적으로 가져온 데이터를 통해 UI를 랜더링하는 코드의 예시를 보자.
const UserProfile = () => {
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setIsLoading(true);
const { data } = await axios.get("/user");
setUser(data);
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
};
fetchUser();
}, []);
if (isLoading) return <Loader />;
if (isError) return <Error />;
if (!user) return <Empty />;
return <div>{user.userName}</div>;
};
데이터 패칭 과정의 상태를 컴포넌트에서 공유받기 위해 사용되는 isLoading
, isError
에 대한 분기처리로 인해 가독성이 떨어지고 복잡해지는 코드를 마주하게 된다.
이제, Suspense와 Error Boundary를 사용해서 데이터라는 웹 자원에 대한 기다림 처리를 선언적으로 작성해보자.
const UserProfile = () => {
const { data } = axios.get("/user");
return <div>{user.userName}</div>;
};
const App = () => {
return (
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loader />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
};
그럼, 여기서 Suspense, ErrorBoundary는 비동기 요청을 가진 컴포넌트의 요청 상태를 어떻게 공유받을 수 있을까
리엑트 문서 예제를 살펴보면, 컴포넌트가 랜더링을 시도할 때 패칭 값을 읽으려는 시도를 하고, read()
에서 throw된 결과값을 전달받아 상태별 UI를 반환한다.
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; // Suspense fallback UI
} else if (status === "error") {
throw result; // Error Boundary fallback UI
} else if (status === "success") {
return result; // Success UI
}
},
};
}
개인 프로젝트에서 Suspense와 Error Boundary를 적용하는 PR을 오픈했다.
Reference
Last updated