[React] React Suspense

복숭아는딱복·2024년 8월 16일

React

목록 보기
6/7
post-thumbnail

리액트 서스펜스(React Suspense)는 React 16.6 버전에서 도입된 기능으로, 비동기 작업을 더 쉽게 처리할 수 있게 해주는 기능이다. Suspense는 자식 요소가 로드되기 전까지 화면에 대체 UI를 보여준다.

https://ko.react.dev/reference/react/Suspense

동기와 비동기

동기(Synchronous)
동기 프로그래밍은 코드가 순차적으로 실행되는 방식이다. 즉, 한 작업이 완료된 후에야 다음 작업이 실행된다. 동기 방식은 이해하기 쉽고 디버깅이 용이하지만, 긴 계산이 있는 경우에는 전체 프로그램이 지연될 수 있다.

비동기(Asynchronous)
비동기 프로그래밍은 프로그램의 특정 작업이 완료되기를 기다리지 않고 다른 작업을 동시에 수행할 수 있도록 하는 프로그래밍 방식이다. 비동기 작업은 주로 시간이 오래 걸리는 작업, 예를 들어 데이터베이스 쿼리, 파일 I/O, 네트워크 요청 등에서 유용하다.

쿼리(query)
데이터베이스에서 정보를 조회하거나 조작하는 요청을 의미한다. 데이터베이스 쿼리는 데이터를 검색하거나 수정, 추가, 삭제하는 등의 작업을 수행한다.

React Suspense

서스펜스 컴포넌트는 비동기 작업을 진행하는 컴포넌트를 자식 컴포넌트로 갖는다. 자식 컴포넌트가 비동기 작업을 수행하는 동안 fallback에 지정된 컴포넌트를 보여준다. 자식 컴포넌트의 비동기 작업이 끝나면 리렌더링이 일어난다.

Suspense 등장 이전의 로딩 방식

1. 워터폴(Waterfall) 방식의 로딩

아래 예시에서, ProfilePage 컴포넌트에서 useEffect를 통해 최초 렌더링 되었을 때 fetchUser를 통해 유저정보를 불러온다. 불러오는 동안, 유저정보는 null이기 때문에 if문 안에 Loading Profile...이 실행되고 fetchUser가 완료되면 setUser를 통해 user가 업데이트 됨으로 리렌더링이 일어난다. 이때, 아래 return문이 실행되면서 ProfileTimeLine을 불러오고. 똑같은 방식으로 로딩된다.

문제점

이 경우 문제점은 워터폴 방식으로 인해 fetchUser가 완료되어야 ProfileTimeLine이 순차적으로 실행되어 결국 사용자는 fetchUser가 완료될때까지 Loading Profile...을 보고, 이후 fetchPosts가 완료될때까지 또 Loading Posts...을 봐야해서 로딩시간이 길어지게 되어 UX가 안좋게 된다. 만약 fetchUser와 fetchPosts를 비동기적으로 불러온다면 더 빨리 정보들을 볼 수 있었을 것이다.

워터폴(Waterfall) 방식
워터폴의 사전적 의미는 폭포로, 폭포가 위에서 아래로 떨어지듯 소프트웨어 개발 프로세스에서 순차적으로 진행되는 프로세스를 의미한다. 전통적인 접근 방법 중 하나이다.

function ProfilePage() {
	const [user, setUser] = userState(null);

	useEffect(()=>{
		fetchUser().then(u => setUser(u))
	},[])

	if (user === null) {
		return <div>Loading Profile...</div>;
	}

	return (
		<>
			<h1>{user.name}</h1>
			<ProfileTimeLine/>
		</>
	)
}

function ProfileTimeLine() {
	const [posts, setPosts] = userState(null);

	useEffect(()=>{
		fetchPosts().then(p => setPosts(p))
	},[])

	if (posts === null) {
		return <div>Loading Posts...</div>;
	}

	return (
		<>
			<ul>
				{posts.map(post => (
                    <li key={post.id}>
                        <h2>{post.title}</h2>
                        <p>{post.body}</p>
                    </li>
                ))}
			</ul>
		</>
	)
}

2. Promise.all을 사용한 로딩

다음으로 등장한 방식은 Promise.all을 사용해 fetchUser와 fetchPosts를 동시에 병렬적으로 불러오는 방식이다. 이렇게 되면 이전 전통적 방식에서는 하나 하나 정보를 불러왔기 때문에 정보가 하나씩 떠서 오래 기다렸다면, Promise.all은 정보가 로딩 후 한번에 뜨기 때문에 비교적 빠른 로딩처럼 보이게 된다.

한계점

하지만 한계점은, 만약 fetchUser는 1초안에 불러와지고 fetchPosts는 10초만에 불러와진다면 fetchUser는 미리 불러와졌음에도 fetchPosts를 기다리느라 정보를 볼 수 없게 된다.

Promise.all
avaScript에서 비동기 작업을 처리할 때 유용한 메서드로, 여러 개의 Promise 객체를 동시에 처리할 수 있게 해준다. Promise.all은 여러 개의 Promise를 배열로 받아서, 모든 Promise가 성공적으로 이행될 때까지 기다린다. 모든 Promise가 이행되면, 그 결과를 배열로 반환한다. 만약 하나라도 실패하면, 전체 Promise.all 호출이 실패한다.

  function fetchProfileData() {
	return Promise.all([
		fetchUser(),
		fetchPosts()
	]).then(([user, posts])=>(
		return (user, posts)
	))
}

Suspense 등장

앞서 설명한 방식들은 먼저 fetch를 하고 fetch가 끝나야 렌더링을 시작했다. 하지만 Suspense를 사용하면 fetch와 동시에 렌더링을 할 수 있다. 렌더링을 시작하면서 fetch가 끝나길 기다리게 해주는 애가 바로 Suspense이다.

const resource = fetchProfileData();

function ProfilePage() {
	return (
        <Suspense fallback=(<span>Loading Profile...</span>)>
			<ProfileDetails/>
			<Suspense fallback=(<span>Loading Posts...</span>)>
			    <ProfileTimeLine />
			</Suspense>
		</Suspense>
    );
}
  • children: 궁극적으로 렌더링하려는 실제 UI
  • fallback: 실제 UI가 로드되기 전까지 대신 렌더링 되는 대체 UI

자식 컴포넌트의 비동기 작업이 끝나면 리렌더링이 일어나고, 비동기가 먼저 처리되는 순서대로 노출이 된다. 이것으로 기존의 한계점을 극복할 수 있다. 예시에서 Suspense는 ProfileDetails를 읽고 패치가 안끝났으면 Loading Profile...를 보여준다. 패치가 진행되는 동안 Suspense는 ProfileTimeLine를 읽고 패치가 안끝났으면 Loading Posts...를 보여준다. 그리고 둘 중 먼저 패치가 끝나는 자식 컴포넌트를 순차적으로 리렌더링한다.

const resource =fetchProfileData();

function ProfilePage() {
	return (
        <Suspense fallback=(<span>Loading...</span>)>
			<ProfileDetails/>
			<ProfileTimeLine />
		</Suspense>
    );
}

이런식으로 한 Suspense 안에 두개의 자식 컴포넌트가 있다면 두 컴포넌트가 모두 패치가 끝나면 리렌더링이 일어난다. 이를 통해 로딩 시점을 조절할 수 있다.

참고 및 인용

https://www.youtube.com/watch?v=8q7OQSPLF4k

0개의 댓글