초간단 비동기 렌더링 React Suspense(By. Boaz) 정리

혜혜·2023년 9월 29일
1

React

목록 보기
5/9
post-thumbnail

💡 해당 글은 유튜브 "가장 쉬운 웹개발 with Boaz"의 "초간단 비동기 렌더링 React Suspense"를 보고 정리한 글입니다. 문제 시 삭제합니다!

초간단 비동기 렌더링 React Suspense

💡 Suspense 개요

const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
  	<ProfilePage />
</Suspense>
  • lazy하게 로딩되고 있는 ProfilePage 컴포넌트
  • 이 컴포넌트를 Suspense의 자식 컴포넌트로 넣어서, 이 비동기 작업이 다 완료되기 전까지 Suspensefallback에 있는 컴포넌트가 렌더링됨
  • 이때 ProfilePage의 비동기 작업이 끝나면, 리렌더링이 일어나서, ProfilePage를 보여줌

📍 요약

  • Suspense는 비동기 작업을 포함하는 컴포넌트를 자식 컴포넌트로 가짐
  • 그 자식 컴포넌트가 비동기 작업을 진행하는 동안, fallback에 할당한 특정 컴포넌트 렌더링
  • React에게 비동기 작업을 하고 있는 자식 컴포넌트를 알려줌
  • React는 해당 컴포넌트의 비동기 작업이 끝나면 리렌더링 수행

💡 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>
    );
}
  • 서버로부터 Profile Data를 fetch 해 와서 resource 변수에 할당
  • 서버로부터 받아온 데이터 중 posts 내용은 ProfileTimeline 컴포넌트에서, user 내용은 ProfileDetails 컴포넌트에서 렌더링 하고 있음

📍 Suspense를 사용하지 않는 전통적인 방법(Suspense를 사용하게 된 이유)

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
  • 두 작업이 모두 끝나면 return
// 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 데이터를 다 못 받아와서 미리 볼 수 없는 문제가 발생함

  • 그래서 나온 방법이 Suspense다!

3. Render-as-You-Fetch

✔️ fetch를 하면서 render를 할 수 있다!!

  • 앞서 봤던 순서는,
  1. fetching 시작
  2. fetching 끝
  3. rendering 시작

즉, fetching이 끝나야지만 렌더링이 시작됨

  • Suspense를 이용하면,
  1. fetching 시작
  2. rendering 시작
  3. fetching 끝
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에 할당되어 있는 컴포넌트를 렌더링
  • 현재 fetch는 계속 진행중!
  • 비동기 작업이 완료되면, ProfileDetails 컴포넌트 리렌더링
  • 그 다음 컴포넌트도 똑같은 과정으로 진행됨

결론적으로 userposts 데이터를 둘 다 기다릴 필요가 없는 것! 작업이 완료되는 순서대로 자식 컴포넌트 노출 가능!

    	<Suspense fallback={<h1>Loading profile...</h1>}>
      		<ProfileDetails />
      		<ProfileTimeline />
      	</Suspense>
  • 만약 이런 식으로 작성되어 있으면, 정보를 둘 다 받아올 때까지 fallback이 렌더링됨

즉, 내가 Loading 상태를 보여주고 싶은 컴포넌트의 "바운더리"를 정하는 개념이라고 생각하면 됨

💡 Suspense는 어떻게 자식 컴포넌트를 알아볼까?

예제 속 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;
        },
    };
};
  • "pending" 상태가 되면 suspender를 throw 하는 것
  • 이러한 특수한 객체를 렌더링 하기 때문에, React가 비동기 작업을 하는 컴포넌트를 인식할 수 있는 것
profile
쉽게만살아가면재미없어빙고

0개의 댓글