React에 Suspense 컴포넌트가 추가되었습니다.
기존의 명령형으로 loading 상태를 지정해 주어야 했던 것을
Suspense 컴포넌트를 이용하면 선언형으로 코드를 작성할 수 있습니다.
React의 experimental에서 사용이 가능했었지만 3월 24일 정식 출시된 18버전에서 사용이 가능합니다.
Concurrent 모드에서 사용이 가능합니다.
기존에 ReactDom.render => ReactDom.createRoot().render 형식으로 사용하여야 Suspense 컴포넌트를 사용할 수 있습니다.
// 기존
import ReactDOM from 'react-dom'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
)
// 변경된 모습
import ReactDOM from 'react-dom/client'
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Concurrent Mode는 어떤것일까요??
간단하게 살펴보자면 렌더링에 인터럽트 개념이 들어온 것입니다.
Concurrent는 동시에 발생하는 이라는 뜻입니다.
잠시 os의 이야기를 하자면
cpu의 자원을 소유하지 않은 프로세스는 cpu의 자원을 소유하고자 cpu에게 인터럽트를 통해 알립니다.
인터럽트를 받은 cpu는 인터럽트 테이블에 명시된 규칙에 따라 현재 점유중인 프로세스에서 인터럽트를 발생시킨 프로세스를 수행합니다.
예를 들자면 네트워크에서의 TCP/IP 요청은 OS의 커널영역에 위치한 프로토콜 스택에서 요청을 보냅니다.
cpu는 프로토콜 스택을 보내 요청을 보내고 다른 프로세스를 수행합니다.
만약 응답이 오면 프로토콜 스택에서는 cpu에게 인터럽트를 보내고 cpu는 응답을 처리합니다.
인터럽트를 발생시킬수 있음에 따라 cpu는 응답을 계속 기다리지 않고 다른 작업을 수행할 수 있습니다.
따라서 사용자는 여러 프로그램이 동시에 실행되는 것처럼 느끼게 됩니다.
이러한 인터럽트 개념이 렌더링에도 들어온 것입니다.
기존에는 상태가 변화되면 렌더링이 일어나게 되고 이러한 렌더링은 중간에 멈출수가 없었습니다.
그래서 입력과 동시에 렌더링이 발생하는 상황이였다면 렌더링이 일어나는 중에는 입력을 할 수 없으므로 버벅거림을 느낄수 있었을 겁니다.
Concurrent모드는 렌더링을 인터럽트가 가능하도록 만들어 줍니다.
React의 Concurrent 모드에서는 입력이 발생하면 입력에 대한 업데이트를 Paint 하고 메모리 내에 있는 업데이트 목록을 계속 렌더링 할 수 있도록 합니다. 렌더링이 끝나면 React는 DOM을 업데이트하고 변경사항들을 화면에 반영합니다.
Concurrent모드에서는 두가지 시나리오가 가능하게 해줍니다.
1. 현재 렌더링 중이라면 중단하고 우선순위가 높은 렌더링이 먼저 보여줄 수 있습니다.
2. 모든 데이터가 도달하기 전에 메모리에서 렌더링을 시작할 수 있고 로딩 상태 표시를 보여주지 않을 수도 있습니다.
자세한 실행흐름은 아래에서 코드의 실행 순서를 보며 알아보겠습니다.
아래에서 살펴볼 예시는 I/O 상황에 대한 Suspense의 적용입니다.
사용은 간단합니다.
Suspense 컴포넌트로 감싸주고 fallback시에 보여줄 컴포넌트를 지정해주면 됩니다.
import { Suspense } from "react";
import { fetchProfileData } from "./apis";
const resource = fetchProfileData();
const ProfilePage = () => {
return (
<Suspense fallback={<h1>Loading profile....</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts....</h1>}>
<ProfilePosts />
</Suspense>
</Suspense>
);
}
const ProfileDetails = () => {
const user = resource.user.read()
return <h1>{user.name}</h1>
}
const ProfilePosts = () => {
const posts = resource.posts.read()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.text}</li>
))}
</ul>
)
}
export default ProfilePage;
비교를 위해 같은 코드를 useEffect로 처리한 코드를 첨부하겠습니다.
const ProfileDetails = () => {
const user = resource.user.read();
if (!user) {
return <h1>Loading profile...</h1>
}
return <h1>{user.name}</h1>;
};
const ProfilePosts = () => {
const posts = resource.posts.read();
if (!posts) {
return <h1>Loading posts...</h1>
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
};
예외처리를 따로 지정해줘야 함을 확인할 수 있습니다.
네트워크를 통해 조회되는 데이터는 크게 3가지 상태를 가질 수 있습니다. fetching, succes, failed
기존에는 fetching, failed일때 다른 컴포넌트를 렌더링 해주어 사용자에게 네트워크의 진행상태를 보여줄 수 있도록 하였습니다.
아래에 예시가 있습니다.
import { useQuery } from 'react-query';
const TodoPage = () => {
const { isLoading, isError, data } = useQuery(
["todo", 1],
async () => await fetch("https://jsonplaceholder.typicode.com/todos/1").then(r => r.json()),
)
if (isLoading) return <h1>Loading</h1>
if (isError) return <h1>Something Wrong</h1>
return (
<div>
{data.title}
</div>
)
}
export default TodoPage
가져와야 하는 데이터가 많아진다면 각 데이터 별로 상태를 가지게 되고 예외처리를 해주어야 할 상황은 기하급수적으로 많아질 수 있습니다.
데이터 2종류면 4가지 상황, 데이터 3종류면 8가지 상황 .....
그리고 명령형으로 코드가 작성되므로 가독성도 떨어집니다.
이 모든 단점을 Suspense를 이용해 해결할 수 있습니다.
Suspense를 사용하면 데이터의 로딩 상태, 여기서는 설명하지 않지만 ErrorBoundary를 사용하면 에러상태를 catch 할 수 있으므로
컴포넌트 내에서는 데이터의 무결성을 보장받을수 있게 됩니다.
아래와 같이 작성이 가능해집니다.
import { useQuery } from 'react-query';
const TodoPage = () => {
const { data } = useQuery(
["todo", 1],
async () => await fetch("https://jsonplaceholder.typicode.com/todos/1").then(r => r.json()),
{
suspense: true,
})
// 이 아래부터는 데이터가 존재함을 보장받을 수 있게 된다
return (
<div>
{data.title}
</div>
)
}
export default TodoPage
// 생략
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Spinner />}>
<TodoPage />
</Suspense>
</ErrorBoundary>
// 생략
Concurrent Mode에서 발생하는 코드 동작을 단계별로 살펴보겠습니다.
import { Suspense } from "react";
import { fetchProfileData } from "./apis";
const resource = fetchProfileData();
const ProfilePage = () => {
return (
<Suspense fallback={<h1>Loading profile....</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts....</h1>}>
<ProfilePosts />
</Suspense>
</Suspense>
);
}
const ProfileDetails = () => {
const user = resource.user.read()
return <h1>{user.name}</h1>
}
const ProfilePosts = () => {
const posts = resource.posts.read()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.text}</li>
))}
</ul>
)
}
export default ProfilePage;
1. React는 ProfilePage 렌더링을 시도합니다. 자식 컴포넌트인 ProfileDetails, ProfilePosts가 반환됩니다.
2. React는 ProfileDetails 렌더링을 시도합니다. resource.user.read()를 호출합니다. 아직 불러온 데이터가 없으므로 이 컴포넌트는 "정지" 합니다. React에서는 이 컴포넌트를 넘기고, 트리 상의 다른 컴포넌트의 렌더링을 시도합니다.
3. React는 ProfilePosts 렌더링을 시도합니다. resoure.posts.read()를 호출합니다. 아직 불러온 데이터가 없으므로 이 컴포넌트는 "정지" 합니다. React는 이 컴포넌트를 넘기고, 트리 상의 다른 컴포넌트의 렌더링을 시도합니다.
4. 렌더링을 시도할 컴포넌트가 없음을 확인 하였습니다. <ProfileDetails>가 정지된 상태이므로 React는 ProfileDetails에서 가장 가까운 Suspense의 Fallback을 찾고 화면에 보여줍니다.
5. resource.user.read()를 불러오고 나면 정지되어 있던 <ProfileDetails> 렌더링을 시도합니다. 렌더링이 완료되고 Fallback은 사라집니다.
위의 흐름대로 React에서는 렌더링을 시도합니다.
Suspense를 사용하기 위해서는 swr, react-query에서 제공하는 suspense 옵션이 있는지 확인해야 합니다.
아래에 첨부한 예제는 위에서 처리한 api이고 promise를 이용하여 suspense를
pending 상태일때는 promise를 throw 해주는 것을 확인할 수 있습니다.
export function fetchProfileData() {
let userPromise = fetchUser();
let postsPromise = fetchPosts();
return {
user: wrapPromise(userPromise),
posts: wrapPromise(postsPromise)
}
}
const wrapPromise = (promise) => {
let status = "pending"
let result
let suspender = promise.then(
(r) => {
status = "success";
result = r;
},
(e) => {
status = "error";
result = e;
})
return {
read() {
console.log(status)
if (status === 'pending') {
console.log(suspender)
throw suspender;
}
if (status === 'success') {
return result;
}
if (status === 'error') {
throw result;
}
}
}
}
const fetchUser = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: "WooBottle"
})
}, 3000)
})
}
const fetchPosts = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 0, text: "this is first" },
{ id: 1, text: "this is second" },
{ id: 2, text: "this is third" },
]);
}, 5000);
});
}
제공할 수 있도록 한 코드입니다.
출처 : https://ko.reactjs.org/docs/concurrent-mode-suspense.html
https://ko.reactjs.org/docs/concurrent-mode-intro.html
https://ko.reactjs.org/docs/concurrent-mode-adoption.html
'Frontend > React' 카테고리의 다른 글
React hook 🌱 (0) | 2022.04.14 |
---|---|
React 연속된 요청 useEffect 내에서 처리하기 (useEffect, suspense) (0) | 2022.04.10 |
React 상태관리의 과거, 현재 그리고 미래 (번역글) (0) | 2022.04.09 |
Recoil 🌱 (0) | 2022.04.08 |
React 상태관리🌱외 minor concept (0) | 2022.04.07 |