// Home
export const Home = () => {
const { data: characters, status } = useFetch({
fetchFunction: fetchCharacters,
args: [50],
cacheKey: ROUTE_PATH.HOME,
});
const charactersList = characters?.results;
return (
<S.Container>
// 비동기 요청이 처리될 때까지 Loading 컴포넌트 렌더링
{status === 'pending' ? (
<Loading />
) : (
<S.Characters>
// Detail
export const Detail = () => {
const { id } = useParams();
const { data: characterDetail, status } = useFetch({
fetchFunction: fetchCharacterDetail,
args: [id],
cacheKey: `${ROUTE_PATH.DETAIL}/${id}`,
});
const detailsInfo = characterDetail?.results[0];
return (
<>
// 비동기 요청이 처리될 때까지 Loading 컴포넌트 렌더링
{status === 'pending' ? (
<Loading />
) : (
<S.Container>
React의
Suspense
와lazy
기능을 사용하여 이러한 문제를 효과적으로 해결할 수 있다.
비동기 작업의 결과를 기다리는 경계
를 설정하는 컴포넌트다.비동기 경계 설정
: Suspense 내부의 컴포넌트들이 데이터를 불러오는 동안 대체 컴포넌트를 보여준다.
이를 통해 비동기 작업 중에도 사용자에게 즉시 반응하는 UI를 제공할 수 있다.
통합된 로딩 상태 관리
: 전통적으로 각 컴포넌트 내에서 로딩 상태를 관리해야 했으나,
Suspense를 사용하면 애플리케이션 전체에서 중앙 집중식으로 로딩 상태를 관리할 수 있다.
코드 분할과 연계
: React의 lazy 함수와 결합하여 컴포넌트의 코드 분할을 쉽게 할 수 있다.
이렇게 하면 특정 컴포넌트가 실제로 렌더링 될 때까지 해당 컴포넌트의 코드를 로딩하지 않아, 초기 로딩 성능이 향상된다.
import { Suspense } from 'react';
import Albums from './Albums.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<Albums artistId={artist.id} />
</Suspense>
</>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
Promise
를 throw
해야한다.pending
를 throw
한다.pending
이면 fallback
에 지정된 컴포넌트를 렌더링 한다.fulfilled
되면) 해당 컴포넌트(Albums)를 렌더링 한다.import { Suspense, lazy } from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
import { useState, useEffect, useRef } from 'react';
import { useCacheContext } from '@/contexts/CacheContext';
type Status = 'initial' | 'pending' | 'fulfilled' | 'rejected';
interface UseFetch<T> {
data?: T;
status: Status;
error?: Error;
cacheKey: string;
}
interface FetchOptions<T> {
fetchFunction: (...args: any[]) => Promise<T>;
args: any[];
cacheKey: string;
}
export const useFetch = <T>({
fetchFunction,
args,
cacheKey,
}: FetchOptions<T>): UseFetch<T> => {
const [state, setState] = useState<UseFetch<T>>({
status: 'initial',
data: undefined,
error: undefined,
cacheKey,
});
const { cacheData, isDataValid } = useCacheContext();
const activePromise = useRef<Promise<void> | null>(null);
useEffect(() => {
let ignore = false;
const fetchData = async () => {
if (ignore) return;
try {
const response = await fetchFunction(...args);
cacheData(cacheKey, response);
setState((prevState) => ({
...prevState,
status: 'fulfilled',
data: response,
cacheKey,
}));
} catch (error) {
setState((prevState) => ({
...prevState,
status: 'rejected',
error: error as Error,
cacheKey,
}));
}
};
if (state.status === 'initial') {
if (isDataValid(cacheKey)) {
setState((prevState) => ({
...prevState,
status: 'fulfilled',
data: cacheData(cacheKey),
cacheKey,
}));
} else {
setState((prevState) => ({
...prevState,
status: 'pending',
}));
activePromise.current = fetchData();
}
}
return () => {
ignore = true;
};
}, [fetchFunction, cacheKey, cacheData, isDataValid, state.status]);
if (state.status === 'pending' && activePromise.current) {
throw activePromise.current;
}
if (state.status === 'rejected' && state.error) {
throw state.error;
}
return state;
};
throw
할 Promise를 useRef
에 저장한다.pending
이고 activePromise.current에 값이 있으면 해당 Promise를 던져 Suspense를 활성화시킨다.rejected
이고 오류가 있으면 해당 오류를 던져서 Error Boundary에서 처리할 수 있게 한다.// Home
export const Home = () => {
const { data: characters } = useFetch({
fetchFunction: fetchCharacters,
args: [50],
cacheKey: ROUTE_PATH.HOME,
});
const charactersList = characters?.results;
return (
<S.Container>
<S.Characters>
// 렌더링 코드 중략
// Detail
export const Detail = () => {
const { id } = useParams();
const { data: characterDetail } = useFetch({
fetchFunction: fetchCharacterDetail,
args: [id],
cacheKey: `${ROUTE_PATH.DETAIL}/${id}`,
});
const detailsInfo = characterDetail?.results[0];
return (
<>
{detailsInfo && (
<S.Container>
// 렌더링 코드 중략
import { Suspense, lazy } from 'react';
import { createBrowserRouter } from 'react-router-dom';
import { Layout } from '@/routes/Layout';
import { ROUTE_PATH } from '@/router/routePath';
import { NotFound } from '@/routes/NotFound';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { Loading } from '@/components/Loading';
import Home from '@/routes/Home';
const Detail = lazy(() => import('@/routes/Detail'));
export const router = createBrowserRouter([
{
element: <Layout />,
path: ROUTE_PATH.ROOT,
errorElement: <NotFound />,
children: [
{
path: ROUTE_PATH.HOME,
element: (
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<Home />
</Suspense>
</ErrorBoundary>
),
},
{
path: ROUTE_PATH.DETAIL,
element: (
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<Detail />
</Suspense>
</ErrorBoundary>
),
},
],
},
]);