데이터를 불러오는 동안 화면에 무엇을 보여 줄 것인가. 흔한 질문이지만 React에서는 이 질문의 답이 컴포넌트마다 흩어져 있었습니다. Suspense는 그 답을 한곳으로 모읍니다.
Suspense 없이 로딩 처리하기#
가장 흔한 방법은 컴포넌트가 직접 데이터를 불러오고, 그 결과를 자기 상태에 담는 것입니다.
import { useState, useEffect } from 'react';
function DataComponent() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
setData(await response.json());
}
fetchData();
}, []);
if (data === null) {
return <div>로딩중...</div>;
}
return <div>{data}</div>;
}
import { useState, useEffect } from 'react';
function DataComponent() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
setData(await response.json());
}
fetchData();
}, []);
if (data === null) {
return <div>로딩중...</div>;
}
return <div>{data}</div>;
}
잘 동작합니다. 다만 두 가지가 걸립니다.
걸리는 점 두 가지#
컴포넌트가 기다리는 일까지 맡는다#
이 컴포넌트가 하려는 일은 데이터를 보여 주는 것입니다. 그런데 코드의 절반은 데이터가 왔는지 확인하는 일입니다. 에러 처리와 재시도까지 넣으면 본래 하려던 일은 더 작아집니다.
로딩 표시를 한곳에서 정할 수 없다#
컴포넌트마다 로딩 상태를 따로 가지면 화면 곳곳에서 "로딩중..."이 제각각 깜빡입니다. 작성자 영역과 본문 영역을 묶어 로딩을 한 번만 보여 주고 싶다면, 두 컴포넌트의 상태를 위로 끌어올려 합쳐야 합니다.
Suspense: 기다리는 일을 밖으로 옮긴다#
Suspense를 쓰면 컴포넌트는 데이터가 이미 있다고 가정하고 씁니다. 아직 없을 때 무엇을 보여 줄지는 바깥의 <Suspense>가 정합니다. 아래는 react-query의 suspense 옵션을 쓴 예입니다.
import { Suspense } from 'react';
import { useQuery } from 'react-query';
function DataComponent() {
// 로딩 상태를 확인하지 않는다.
const { data } = useQuery('data', fetchSomeData, { suspense: true });
return <div>{data}</div>;
}
function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<DataComponent />
</Suspense>
);
}
import { Suspense } from 'react';
import { useQuery } from 'react-query';
function DataComponent() {
// 로딩 상태를 확인하지 않는다.
const { data } = useQuery('data', fetchSomeData, { suspense: true });
return <div>{data}</div>;
}
function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<DataComponent />
</Suspense>
);
}
첫 번째 걸림돌이 사라졌습니다. DataComponent에는 로딩을 확인하는 코드가 없습니다.
두 번째도 풀립니다. 로딩을 어디서 어떤 단위로 보여 줄지는 <Suspense>를 어디에 두느냐로 정해집니다.
function ArticleAuthor() {
const { data: author } = useQuery('author', fetchAuthor, { suspense: true });
return <div>{author}</div>;
}
function ArticleContent() {
const { data: content } = useQuery('content', fetchContent, { suspense: true });
return <div>{content}</div>;
}
function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<ArticleAuthor />
<ArticleContent />
</Suspense>
);
}
function ArticleAuthor() {
const { data: author } = useQuery('author', fetchAuthor, { suspense: true });
return <div>{author}</div>;
}
function ArticleContent() {
const { data: content } = useQuery('content', fetchContent, { suspense: true });
return <div>{content}</div>;
}
function App() {
return (
<Suspense fallback={<div>로딩중...</div>}>
<ArticleAuthor />
<ArticleContent />
</Suspense>
);
}
둘을 한 경계로 감싸면 로딩은 한 번만 보이고, 따로 감싸면 준비된 쪽부터 나타납니다. 컴포넌트는 고치지 않고 경계의 위치만 바꾸면 됩니다.
도입하기 전에#
- Suspense가 알아듣는 데이터 소스여야 합니다.
useEffect 안에서 fetch하는 코드는 <Suspense>로 감싸도 fallback이 뜨지 않습니다. react-query처럼 Suspense를 지원하도록 만들어진 라이브러리를 써야 합니다. - 한 컴포넌트 안에서 suspense 모드의
useQuery를 여러 번 쓰면 요청이 차례로 나갑니다. 첫 번째에서 렌더링이 멈추기 때문에 두 번째는 시작도 하지 못합니다. 이럴 때는 useQueries로 묶거나 컴포넌트를 나눕니다. - 에러는 Suspense가 아니라 Error Boundary가 받습니다. 로딩을 밖으로 옮겼다면 에러도 밖에서 받을 준비를 해야 합니다.
동작 원리는 React Suspense의 내부 동작방식에서 이어집니다.