React - Suspense

sarang_daddy·2023년 10월 16일
0

React

목록 보기
22/26

Why Suspense?

  • 기존에는 각 컴포넌트에서 데이터를 비동기로 요청할 때, 해당 컴포넌트 내에서 로딩 상태를 관리했다.
// 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의 Suspenselazy 기능을 사용하여 이러한 문제를 효과적으로 해결할 수 있다.

Suspense

  • Suspense는 React에서 비동기 작업의 결과를 기다리는 경계를 설정하는 컴포넌트다.
  • 주로 lazy와 함께 사용하여 코드 분할 및 비동기 컴포넌트 로딩을 처리하며,
  • 데이터를 불러오는 동안 보여줄 대체 컴포넌트 (보통 로딩 인디케이터)를 설정하는 데 사용된다.

Suspense의 주요 특징

  • 비동기 경계 설정
    : Suspense 내부의 컴포넌트들이 데이터를 불러오는 동안 대체 컴포넌트를 보여준다.
    이를 통해 비동기 작업 중에도 사용자에게 즉시 반응하는 UI를 제공할 수 있다.

  • 통합된 로딩 상태 관리
    : 전통적으로 각 컴포넌트 내에서 로딩 상태를 관리해야 했으나,
    Suspense를 사용하면 애플리케이션 전체에서 중앙 집중식으로 로딩 상태를 관리할 수 있다.

  • 코드 분할과 연계
    : React의 lazy 함수와 결합하여 컴포넌트의 코드 분할을 쉽게 할 수 있다.
    이렇게 하면 특정 컴포넌트가 실제로 렌더링 될 때까지 해당 컴포넌트의 코드를 로딩하지 않아, 초기 로딩 성능이 향상된다.

Suspense 사용법

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>;
}
  • Suspense 경계 내의 컴포넌트 Albums는 비동기적으로 데이터를 요청한다.
  • Albums는 비동기 작업 중일 때 해당 Promisethrow 해야한다.
  • Albums는 데이터를 아직 받지 못했다면 Promise 가 pendingthrow한다.
  • Suspense는 Albums가 던진 Promise 상태를 받아 pending이면 fallback에 지정된 컴포넌트를 렌더링 한다.
  • 데이터가 준비되면 (Promise가 fulfilled되면) 해당 컴포넌트(Albums)를 렌더링 한다.

lazy

  • lazy는 React의 기능 중 하나로, 컴포넌트를 동적으로 로드하는 데 사용된다.
  • 이는 앱의 초기 로드 시 필요하지 않은 컴포넌트를 로드하지 않아 번들 크기를 줄이고 초기 로딩 속도를 개선할 수 있다.
  • 사용자가 특정 기능이나 뷰에 액세스할 때만 해당 컴포넌트를 로드한다.

lazy 사용법

import { Suspense, lazy } from 'react';

const OtherComponent = lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}
  • 동적으로 불러내고자 하는 컴포넌트(OtherComponent)를 lazy 함수로 호출한다.
  • lazy는 Suspense 컴포넌트와 함께 사용된다.
  • 사용자가 OtherComponent를 요청하면 로드되기에 로드되는 동안 Suspense의 fallback 컴포넌트를 보여준다.

🚀 코드에 적용하기

1. 데이터 요청 useFetch에서 Promise를 throw 하도록 수정

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에 저장한다.
  • Promise가 pending 상태가 되면 activePromise를 업데이트 해준다.
  • Promise 상태가 pending이고 activePromise.current에 값이 있으면 해당 Promise를 던져 Suspense를 활성화시킨다.
  • Promise 상태가 rejected이고 오류가 있으면 해당 오류를 던져서 Error Boundary에서 처리할 수 있게 한다.

2. 각 컴포넌트에서 관리하던 로딩 상태 로직을 제거한다.

// 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>
        // 렌더링 코드 중략

3. router 로직에서 비동기 요청 처리로 인한 로딩 상태를 관리한다.

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>
        ),
      },
    ],
  },
]);
  • lazy를 사용하여 Detail 컴포넌트는 초기 로드에서 제외하고 사용자가 엑세스 하는 경우 로드한다.
profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글