💡 해당 글은 유튜브 "가장 쉬운 웹개발 with Boaz"의 "초간단 비동기 렌더링 React Suspense"를 보고 정리한 글입니다. 문제 시 삭제합니다!
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded
// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
ProfilePage 컴포넌트Suspense의 자식 컴포넌트로 넣어서, 이 비동기 작업이 다 완료되기 전까지 Suspense의 fallback에 있는 컴포넌트가 렌더링됨ProfilePage의 비동기 작업이 끝나면, 리렌더링이 일어나서, ProfilePage를 보여줌Suspense는 비동기 작업을 포함하는 컴포넌트를 자식 컴포넌트로 가짐fallback에 할당한 특정 컴포넌트 렌더링const resource = fetchProfileData();
function ProfilePage(){
return(
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails(){
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline(){
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => {
<li key={post.id}>{post.text}</li>
})}
</ul>
);
}
resource 변수에 할당posts 내용은 ProfileTimeline 컴포넌트에서, user 내용은 ProfileDetails 컴포넌트에서 렌더링 하고 있음1. Fetch-on-Render
=> 렌더링 한 직후에 불러오기!
// In a function component:
useEffect(() => {
fetchSomething();
}, []);
function ProfilePage(){
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if(user === null){
return <p>Loading profile...</p>;
}
return(
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
)
}
function ProfileTimeline(){
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if(posts === null){
return <h2>Loading posts...</h2>;
}
return(
<ul>
{posts.map(post => {
<li key={post.id}>{post.text}</li>
})}
</ul>
);
}
ProfilePage 컴포넌트를 호출하면, 일단 user에 null 값이 들어감useEffect()를 통해 fetchUser().then() 호출user가 null인 동안에는, 'Loading profile...'을 보여줌useEffect()의 작업이 완료가 되면 로직상 user state가 바뀌게 되고, state가 바뀌었기 때문에 리렌더링 수행ProfilePage가 다시 한번 호출이 되고, 이때는 user가 null이 아니므로 맨 아래가 return 됨 -> ProfileTimeline 호출ProfileTimeline은 위와 거의 똑같은 과정으로 진행됨이 방법의 문제점
fetchUser()로user정보를 받아오는 것은 비동기 작업이기 때문에, 이론상fetchPosts()를 통해posts정보를 받아오는 작업도 병렬적으로 수행이 가능함에도 불구하고, 코드의 구조적인 한계 때문에 순차적으로 실행해야 함
2. Fetch-Then-Render
function fetchProfileData(){
return Promise.all([
fetchUser(),
fetchPosts()
]).then(({user, posts}) => {
return {user, posts};
})
}
user, posts 정보를 서버로부터 동시에 fetching// Kick off fetching as early as possible
const promise = fetchProfileData();
function ProfilePage(){
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then(data => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if(user === null) {
return <p>Loading profile...</p>;
}
return(
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
)
}
// The child doesn't trigger fetching anymore
function ProfileTimeline({ posts }){
if(posts === null){
return <h2>Loading posts...</h2>;
}
return(
<ul>
{posts.map(post => {
<li key={post.id}>{post.text}</li>
})}
</ul>
);
}
user, posts 데이터를 동시에 다 받아옴user, posts 정보를 동시에 볼 수 있다!이 방법의 문제점
만약
fetchPosts()가 10초 정도가 걸리는 비동기 작업이고,fetchUser()은 그보다 빠르다고 가정했을 때,user데이터는 이미 받아왔음에도 불구하고,posts데이터를 다 못 받아와서 미리 볼 수 없는 문제가 발생함
3. Render-as-You-Fetch
✔️ fetch를 하면서 render를 할 수 있다!!
즉, fetching이 끝나야지만 렌더링이 시작됨
Suspense를 이용하면,const resource = fetchProfileData();
function ProfilePage(){
return(
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails(){
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline(){
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => {
<li key={post.id}>{post.text}</li>
})}
</ul>
);
}
Suspense는 비동기 작업이 끝나지 않은 컴포넌트를 React에 등록해놓는 개념!!ProfileDetails -> ProfileTimeline 차례로 등록하고, 더이상 자식 컴포넌트가 없을 때까지 쭉 렌더링Suspend에 등록을 한 컴포넌트(ProfileDetails)의 부모 Suspense 컴포넌트를 찾고, 그 컴포넌트의 fallback에 할당되어 있는 컴포넌트를 렌더링ProfileDetails 컴포넌트 리렌더링결론적으로
user와posts데이터를 둘 다 기다릴 필요가 없는 것! 작업이 완료되는 순서대로 자식 컴포넌트 노출 가능!
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<ProfileTimeline />
</Suspense>
fallback이 렌더링됨즉, 내가 Loading 상태를 보여주고 싶은 컴포넌트의 "바운더리"를 정하는 개념이라고 생각하면 됨
예제 속 resource
// suspense.js
import React from "react";
// Suspense 사용을 위한 promise 통합 객체 만드는 함수
export const createResource = (promise) => {
let status = "pending";
let result;
let suspender = promise.then(
(data) => {
status = "success";
result = data;
},
(err) => {
status = "error";
result = err;
}
);
return {
read(){
if(status === "pending"){
throw suspender;
} else if(status === "error"){
throw result;
}
// status === "success"
return result;
},
};
};
suspender를 throw 하는 것