SWR을 사용한 useRequest, 리엑트 Data Fetching

React Hooks가 나온 이후 Data Fetching을 좀 더 간결하게 하는 라이브러리가 많이 나오는 것 같습니다. 아래는 관련 라이브러리들을 npm trends에서 download 수를 비교해본 결과입니다.

https://www.npmtrends.com/react-async-hook-vs-react-fetch-hook-vs-rest-hooks-vs-swr-vs-use-http-vs-react-query-vs-react-async-vs-react-promise

최근 react-query와 SWR의 증가세가 돋보였습니다. 저는 기능을 조금 비교해본 후 SWR을 도입하기로 했습니다. SWR은 ZEIT에서 만든 Data Fetching 라이브러리인데 기본 소개와 사용법은 SWR 홈페이지에 잘 나와 있습니다.

그동안 CRUD를 개발하면서 사용자경험을 위해 세심하게 구현 코드를 늘려나갔는데요. Data Fetching 부분에 Redux 도 쓰고, 뒤로 가기 시 사용자 경험을 위해 axios에 cache도 하고 점점 복잡해져 갔습니다. 근데 SWR 라이브러리가 많은 것을 한 번에 해결해줬습니다.

SWR github 저장소에 axios와 함께 사용하는 예제가 있었는데 useRequest라는 커스텀 훅을 만들어서 사용합니다. 이 커스텀 훅은 axios 요청을 위한 옵션 객체를 stringify 하여 SWR 의 키로 사용하는 방식입니다.

key 만드는 부분

저는 API Wrapper를 만들어두기를 선호해서 직접 axios로 호출하지 않는데요. 그래서 별도로 래핑 된 API 함수도 함께 적용할 수 있도록 useRequest를 조금 고쳤습니다.

import useSWR from 'swr';
import API, { http } from '~/api';

export default function useRequest(request, { initialData, ...config } = {}) {
  const APIWrapper =
    typeof request?.api === 'string' &&
    request.api.split('.').reduce((a, c) => a[c], API);
  const usingAPIwrapper = typeof APIWrapper === 'function';
  const usingAxios = typeof request?.url === 'string';

  const key = usingAPIwrapper || usingAxios ? JSON.stringify(request) : request;
  const fetcher = () =>
    usingAPIwrapper ? APIWrapper(request.params) : http(request || {});
  const options = {
    ...config,
    initialData: initialData && {
      status: 200,
      statusText: 'InitialData',
      headers: {},
      data: initialData,
    },
  };

  const { data: response, error, isValidating, mutate } = useSWR(
    request && [key, ...(request.deps || [])],
    fetcher,
    options,
  );

  return {
    data: response?.data,
    response,
    error,
    isValidating,
    isLoading: !response?.data && !error,
    mutate,
    key,
  };
}

아래처럼 useRequest에 전달하는 객체에 api라는 이름으로 ‘articles.list’와 같이 API Path 문자열을 넘기면 API Wrapper에 만들어둔 함수를 API.articles.list() 와 같이 실행할 수 있습니다. 그리고 기존 useRequest 처럼 axios 옵션을 전달할 수도 있습니다.

const { data, error, isValidating, mutate } = useReqeust({ api: 'articles.list' });

그리고 Suspense와 ErrorBoundary를 조합하여 아래 샘플처럼 사용합니다.

import React, { Suspense } from 'react';
import { useParams } from 'react-router-dom';
import ErrorBoundary from '~/util/ErrorBoundary';
import useReqeust from '~/util/useReqeust';

const Detail = ({ id }) => {
  const { data } = useReqeust(
    {
      api: 'articles.detail',
      params: id,
    },
    { suspense: true },
  );

  return (
    <table border="1">
      <tbody>
        <tr>
          <th>제목</th>
          <td>{data.subject}</td>
        </tr>
        <tr>
          <th>작성자</th>
          <td>{data.author}</td>
        </tr>
        <tr>
          <th>내용</th>
          <td>{data.content}</td>
        </tr>
      </tbody>
    </table>
  );
};

const ArticleDetail = () => {
  const { id } = useParams();

  return (
    <div style={{ padding: 30 }}>
      <h2>ArticleDetail</h2>
      <ErrorBoundary fallback={<div>내용을 불러올 수 없습니다.</div>}>
        <Suspense fallback={<div>Loading...</div>}>
          <Detail id={id} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
};

export default ArticleDetail;

여기서 단순히 로딩 중임을 표시할 수도 있고, SWR이 캐시 데이터를 갱신할 때 업데이트 중임을 알리는 인디케이터를 붙일 수도 있습니다.


단순히 소개하고 기록해두려는 포스팅이라 때문에 내용이 상당히 부실한데요.. 어쨌든 SWR은 개발자경험을 살리면서 사용자경험도 동시에 향상할 수 있는 좋은 라이브러리인 것 같습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.