React Query v5로 스켈레톤뷰와 페칭해보기

milkbottle·2024년 12월 24일

React

목록 보기
33/33

React Query

React Query는 Tasnstack에서 만든 패키지이다. 이를 사용해서 서버에 쉽게 요청할 수 있다. 하지만 fetch, axios 등 JS API에서 자체적으로 만든 API 요청 기능이 있는데 왜 굳이 써야하는지 알아보자.

설치

pnpm i @tanstack/react-query
v5에서는 v4와 좀 문법이 달라져서 공식문서를 읽어보는 것도 좋다.

기능들

QueryKey 캐싱

React Query는 캐싱기능을 제공한다. 참고로 서버에 요청하는 한 번의 행동을 Query라고 일컫는다.
Query의 결과가 같은데 요청을 굳이 서버에 2번한다면 자원소모가 괜히 커질 것이다.
그래서 이를 캐싱해서 요청하는 것이다.
기존의 요청은 아래의 사진처럼 흐른다.

GET 요청을 하면, React-query 측에선 그대로 전달해서 응답을 다시 받아온다.
그런데 이 요청의 엔드포인트나 쿼리스트링 등 요청의 주소가 같다면? 이젠 React-query가 응답결과(title~~)를 저장해서 아래사진처럼 그대로 전달해주는 것이다.

즉 React-query는 프록시서버와 같은 역할을 해서 프론트엔드의 요청을 확인해 캐싱을 해줄 수 있다.
이 캐싱을 서로 식별할 수 있도록 queryKey가 필요하다.
보통은 [엔드포인트, GET요청이라면 쿼리스트링, POST요청이라면 바디] 정도를 한다.

Suspense

react에서는 Suspense라는 컴포넌트를 제공하는데 하위의 컴포넌트가 로딩상태라면 fallback을 띄우는 역할을 한다.
Layout Shift를 방지하기 위해 스켈레톤뷰를 띄울때 좋다.
하지만 기본적으로 react-query의 로딩상태는 react가 알지못해서, react-query 측에서 react의 Suspense가 로딩상태인지 아닌지 알 수 있도록 useSuspenseQuery라는 것을 제공한다.
참고로 v4.5까지는 useQueries의 suspense가 동작을 안했는데, 이후부터 Github Issue로 해결되었다고 한다.

페이지네이션

useInfiniteQueries로 백엔드측의 API가 페이지네이션 요청일때 이를 편하게 사용할 수 있는 기능도 있다고 한다.

예제 - 공지사항 컴포넌트 구현하기

로스트아크라는 게임에서 Open API로 제공하기에 백엔드를 구현을 안해도 돼서 마치 Open API가 백엔드인것처럼 개발할 것이다.

이렇게 요청과 결과에 대한 좋은 문서 예제가 있어 명세를 보고 구현해보자!

useQuery 구현하기

먼저 요청하는 타입과 응답값을 타입화한다.
interface나 type는 성능차이는 없어서 아무거도 써도 되지만 일단 type을 사용했다.

type GetNoticesParams = {
  searchText?: string;
  type?: '공지' | '점검' | '상점' | '이벤트';
};

type GetNoticesResponse = {
  Title: string;
  Date: string;
  Link: string;
  Type: '공지' | '점검' | '상점' | '이벤트';
};

이를 기반으로 페칭하는 함수를 구현한다.

const getNotices = async (params?: GetNoticesParams): Promise<GetNoticesResponse[]> => {
  const queryString = createQueryString(params);

  const response = await fetch(`/api/notices?${queryString}`, {
    method: 'GET',
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch notices: ${response.statusText}`);
  }

  return response.json();
};

참고로 createQueryString이란 함수는 객체타입을 쿼리스트링으로 펴발라주는 함수로 직접 구현했다. (이정도는 직접 구현해보자)
그리고 사진에서는 /news/notices인데 여기선 왜 /api/notices냐 하면, API KEY를 숨기기 위해 내부 프록시 기능을 사용해서 그렇다. 추후 다른 글에서 설명하겠다.
마지막으로 useQuery를 활용한 useGetNoticesQuery를 만들었다.

export const useGetNoticesQuery = (params?: GetNoticesParams) => {
  return useQuery<GetNoticesResponse[]>({
    queryKey: ['notices', params],
    queryFn: () => getNotices(params),
  });
};

SkeletonView 그리기

이제 페칭하는 useQuery를 만들었으니 컴포넌트에 값을 활용하여 그릴 수 있을 것이다.

const {data} = useGetNoticesQuery({type: '공지'});

return (
    <TitleInfoList
      title={'로스트아크 공지사항'}
      renderInfoList={() => {
        return (
          <ul className="flex w-full flex-col items-start gap-1">
            {notices?.map((notice) => (
              <li key={notice.Title} className="w-full">
                <p className="w-full truncate text-sm">{notice.Title}</p>
              </li>
            ))}
          </ul>
        );
      }}
    ></TitleInfoList>
);

하지만 여기서 문제가 발생한다.


이렇게 덜컥거리는 Layout Shift현상이 일어난다. 그래서 Skeleton View를 그려준다.

const { data: _notices, isLoading } = useGetNoticesQuery({
  type: '공지',
});

const notices = _notices?.slice(0, NOTICES_COUNT);

return (
  <TitleInfoList
    title={'로스트아크 공지사항'}
    renderInfoList={() => {
      if (isLoading) {
        return <SkeletonView width="100%" height="20px" gap="4px" length={NOTICES_COUNT} />;
      }
      return (
        <ul className="flex w-full flex-col items-start gap-1">
          {notices?.map((notice) => (
            <li key={notice.Title} className="w-full">
              <p className="w-full truncate text-sm">{notice.Title}</p>
            </li>
          ))}
        </ul>
      );
    }}
    ></TitleInfoList>
);


이렇게 useQuery에서 자체적으로 제공하는 isLoading을 통해 로딩상태를 반환하므로 이를 활용할 수 있다.
SkeletonView도 직접 구현한 컴포넌트이므로 직접 구현해며 추가적으로 공부해보기 바란다.

useSuspenseQuery 적용하기

<Suspense> 컴포넌트를 적용하고 싶다면, useQuerySuspense의 위치는 달라야한다.
<Suspense> 컴포넌트가 useQuery 훅을 사용하는 컴포넌트보다 상위에 존재해야한다.
하지만 이코드는 현재 같은 위치에 정의되어있다. 그래서 renderInfoList에 해당하는 컴포넌트를 하위의 컴포넌트로 묶어주면 된다.

// useQuery에서 useSuspsneQuery로 변경한다.
export const useGetNoticesQuery = (params?: GetNoticesParams) => {
  return useSuspenseQuery<GetNoticesResponse[]>({
    queryKey: ['notices', params],
    queryFn: () => getNotices(params),
  });
};

// 컴포넌트 내부
const { data: _notices, isLoading } = useGetNoticesQuery({
  type: '공지',
});

const notices = _notices?.slice(0, NOTICES_COUNT);

return (
  <TitleInfoList
    title={'로스트아크 공지사항'}
    renderInfoList={() => 
      <Suspense fallback={<SkeletonView width="100%" height="20px" gap="4px" length={NOTICES_COUNT} />} >
        <하위컴포넌트 data={data} />
      </Suspense>
    }
    ></TitleInfoList>
);

참고

0개의 댓글