React-Router v7 Framework mode에서 loader와 Suspense를 활용한 스트리밍 처리 방법을 알아보겠습니다.
목차는 아래와 같습니다.
- loader + Suspense + Await를 활용한 스트리밍
- loader + Suspense + use API를 활용한 스트리밍
- SEO 최적화를 위해 신경 쓸 점
export async function loader({ params }: Route.LoaderArgs) {
const city = new Promise<{ city: string }>((resolve) => setTimeout(() => resolve({ city: params.city }), 1000));
const description = await new Promise<{ description: string }>((resolve) =>
setTimeout(() => resolve({ description: "one of popular cities in the world" }), 300)
);
return { city, description };
}
export default function City({ loaderData }: Route.ComponentProps) {
const { city, description } = loaderData;
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Await resolve={city}>{(data) => <div>City: {data.city}</div>}</Await>
</Suspense>
<div>Description: {description.description}</div>
</div>
);
}
위 코드의 결과로, description만이 바로 보여지게 되며, city만이 스트리밍됩니다.
Suspense의 자식으로 Await 컴포넌트가 사용되고 있는데요, 그 이유는 pending중인 promise를 throw 하기 위함입니다. 기본적으로 Suspense는 throw된 promise를 받고 지연 상태임을 확인하기 때문에 이 예제에서는 Await 없이는 스트리밍이 동작하지않습니다.
promise가 fullfilled 상태가 되면, fallback UI가 Await 컴포넌트의 자식으로 대체됩니다.
export async function loader({ params }: Route.LoaderArgs) {
const city = new Promise<{ city: string }>((resolve) => setTimeout(() => resolve({ city: params.city }), 1000));
const description = await new Promise<{ description: string }>((resolve) =>
setTimeout(() => resolve({ description: "one of popular cities in the world" }), 300)
);
return { city, description };
}
export default function City({ loaderData }: Route.ComponentProps) {
const { city, description } = loaderData;
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<CityName p={city} />
</Suspense>
<div>Description: {description.description}</div>
</div>
);
}
const CityName = ({ p }: { p: Promise<{ city: string }> }) => {
const data = use(p);
return <div>City Name: {data.city}</div>;
};
use는 Canary에서 실험적으로 사용되던 API였는데요. React 19에서 공식적으로 사용할 수 있게 되었습니다. 사용방법은 크게 차이가 없습니다. 클라이언트 컴포넌트를 생성하고, promise를 prop으로 받은 뒤, use()로 wrap 하면 됩니다. use가 알아서 제일 가까운 Suspense Boundary를 찾아 promise를 throw 해줍니다.
Suspense 사용시 서버에서는 fallback UI를 렌더링하도록 설계됐습니다.
네트워크에서 직접 까봤을 때, 스트리밍을 적용한 부분이 Loading...으로 표기되고 있습니다.
따라서 SSR을 사용할 때, SEO 최적화 관점에서 상대적으로 덜 중요한 데이터에 스트리밍을 적용하고, 중요한 데이터엔 스트리밍을 적용하지않는 것이 중요합니다.