💡 해당 글은 유튜브 "가장 쉬운 웹개발 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 하는 것