ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Portfolly] react-query로 불필요한 서버 요청 감소하기
    Studying/Proj 과정 2023. 12. 13. 22:57

    문제 상황

    기존 Portfolly 홈페이지는 5개의 포트폴리오 카테고리 탭을 바꿀 때마다 서버에 새로운 데이터를 요청합니다.

     

    데이터에 큰 변동이 없음에도 서버에 잦은 요청을 하는 건 꽤 낭비라고 생각했습니다.

     

    아래 영상을 보면 탭을 바꿀 때마다 서버에 GET 요청을 하는 걸 우측 네트워크 대시보드에서 확인할 수 있습니다. 

     

     

     

    해결 방안

    react-query의 데이터 캐싱을 사용해 변화가 적은 포트폴리오 데이터의 재요청 빈도수를 줄입니다.

     

     

     

    구현 과정

    다음과 같이 비동기 데이터를 받는 컴포넌트 <PortfolioCardList />를 불러옵니다.

     

    export default function MainPage(){
    return(
    	...
        <S.PortfolioSection>
            <ApiErrorBoundary FallbackComponent={ApiErrorFallback} onReset={reset}>
                <Suspense fallback={<PortfolioListSkeleton type='portfolio-card' />}>
                    <PortfolioCardList filter={filter} />
                </Suspense>
            </ApiErrorBoundary>
        </S.PortfolioSection>
    );
    }

     

     

    <PortfolioCardList /> 컴포넌트는 usePortfolioQuery 훅을 호출해 포트폴리오 목록 데이터를 받아옵니다.

     

    usePortfolioQuery 이란, react-query의 useQuery 훅을 반환하는 커스텀 훅 입니다.

     

    구조는 다음과 같습니다.

    원래 카테고리, 태그, 키워드 등 필터를 판단하는 로직이 있는데 복잡해보일까봐 잠깐 지웠습니다.

    export const usePortfoliosQuery = (section: Section, filter: {[key in string]: string}) => {
    	// ...생략
    
    	const getPortfolios = ({ pageParam }: { pageParam: number }) => {
    		return fetch(`/portfolios?page=${pageParam}&section=${section}&${filterQueryString}`, 'GET');
    	};
    
    	return useSuspenseInfiniteQuery({
    		queryKey: portfolioKeys.list(section, filter),
    		queryFn: getPortfolios,
    		select: data => data.pages.flat(),
    		initialPageParam: 1,
    		getNextPageParam: (lastPage: any, allPages: any) => {
    			const nextPageNum = allPages.length + 1;
    			return lastPage?.length < PAGE_PER_DATA ? null : nextPageNum;
    		},
    		staleTime: Infinity,
    		gcTime: Infinity,
    	});
    };

     

    무한 스크롤을 사용하기 위해 useSuspenseInfiniteQuery 를 사용했고, 지금 당장 집중할 속성은 queryKey, queryFn, staleTime, gcTime 입니다.

     

    (react-query 버전 5부터 chacheTimegcTime으로 바뀌었습니다. 바뀐 다른 내용은 아래 블로그를 참고하세요)

     

    TanStack Query v5 정식 버전 살펴보기 (리액트 쿼리)

    리액트 쿼리 v5가 정식 출시됐는데요, 이번 글에서는 마이그레이션 가이드를 참고해서 주요 변경 사항들을 살펴봅니다.

    www.moonkorea.dev

     

     

    queryKey를 배열로 관리

    queryKey란 다양한 query를 구분짓는 key 입니다. 특정 query를 불러오거나 수정할 때, 바로 이 queryKey를 사용해 특정 query를 찾아냅니다.

     

    queryKey는 배열로 관리하는 게 좋습니다.

     

    가장 큰 범위부터 점점 작은 범위로 축소되게 지정하면 query를 구분하기 간편해집니다. 저는 아래와 같이 queryKey를 구분했습니다.

    const portfolioKeys = {
      all: ['portfolios'] as const,
      lists: (type: string) => [...portfolioKeys.all, type] as const,
      list: (type: string, filters: string | object) => [...portfolioKeys.lists(type), { filters }] as const,
      details: () => [...portfolioKeys.all, 'detail'] as const,
      detail: (id: string) => [...portfolioKeys.details(), id] as const,
    }

     

    아무 필터 없이 포트폴리오 목록 전체 데이터를 받아올 때는 ['portfolios']

    묶음 별로 가져올 때는 ['portfolios', 묶음 기준]

    특정 묶음을 가져올 때는 ['portfolios', 묶음 기준, 필터]

    포트폴리오 2개 이상을 불러올 때는 ['portfolios', 'detail']

    포트폴리오 1개를 불러올 때는 ['portfolios', 'detail', 아이디] 입니다.

     

    MainPage.tsx에서 불러오는 PortfolioList.tsx는 필터별로 가져오기 때문에 list 형식 키를 지정했습니다.

     

     

    queryKey를 배열로 관리하는 이유와 방법에 대한 더 자세한 내용은 아래 블로그를 참고하면 좋습니다.

     

    React Query Key 관리

    queryKey는 React Query에서 중요한 개념이다. 내부적으로 데이터를 캐시하고 쿼리에 대한 종속성이 변경될 때 자동으로 다시 가져올 수 있게 한다.

    www.zigae.com

     

     

     

    queryFn 지정하기

    queryFn은 Promise 객체를 반환하는 함수입니다. 서버에 비동기 데이터를 요청하는 함수를 할당하면 useQuery가 데이터를 재요청 할 때마다 해당 함수를 호출합니다.

     

    저의 경우 다음과 같은 함수를 지정해줬습니다.

    const getPortfolios = ({ pageParam }: { pageParam: number }) => {
        return fetch(`/portfolios?page=${pageParam}&section=${section}&${filterQueryString}`, 'GET');
    };

     

    fetch는 api를 요청하는 로직을 제가 유틸 함수로 분리시켜 사용하는 함수입니다.

     

    방법은 다른 게시물에 기록했습니다.

     

    API 요청 코드 분리하기(일반 함수 vs 커스텀 훅)

    개요 서버에 API 요청을 할 때, 매번 비슷한 형태의 코드를 작성하기 귀찮아서 주로 call 이라는 이름의 일반 함수(유틸util 함수)를 별도로 분리한 뒤 호출해 사용했습니다. 그런데 문득 일반 함수

    all-done.tistory.com

     

     

     

    카테고리, 태그, 키워드 별 query 저장하기

    저는 queryString으로 filter 값을 관리했습니다.

     

    가장 기본인 섹션 파라미터가 이렇게 있고, /main/android-ios

    카테고리 필터는 /main/android-ios?filter=appCategory.쇼핑

    카테고리 + 태그 + 키워드 필터는 /search/android-ios?filter=tag.tag1_appCategory.비즈니스_keyword.검색어

     

    메인 페이지에서 아래와 같이 쿼리 파라미터로부터 필터를 구합니다.

     

    getFilterQueryString은 filter 쿼리 파라미터를 구하는 유틸 함수입니다.

    const filter = getFilterQueryString();

     

     

    filter를 PortfolioList.tsx에서 usePortfolioQuery의 두 번째 매개변수로 전달하고,

    const {
        data: portfolios,
        fetchNextPage,
        hasNextPage,
    } = usePortfoliosQuery(
        currentSection,
        filter,
        );

     

     

    그리고 usePortfolioQuery 에서 다음과 같이 filter 쿼리 스트링을 뽑아내 서버 요청 url에 덧붙여줍니다.

    let filterQueryString = ``;
    
    // 존재하는 필터를 순회하며 filterQueryString에 이어붙인다.
    Object.keys(filter).forEach((filterType: string) => {
        filterQueryString += `${filterType}=${toUrlParameter(filter[filterType])}&`;
    });
    
    // appCategory 기본값은 '전체'
    if(!filter['appCategory']) {
        filterQueryString += `appCategory=전체&`;
    }
    
    // 마지막 & 기호를 제거한다.
    filterQueryString = filterQueryString.slice(0, -1);

     

     

    queryKey 또한 다음과 같이 filter 값을 받아서, 필터 별 포트폴리오 목록 query를 별도로 캐싱합니다.

    queryKey: portfolioKeys.list(section, filter),

     

     

    결과

    react-query 개발자 도구를 열어보면 다음과 같이 queryKey별로 데이터를 저장했다가, 다시 호출했을 때 staleTime이 유효하면 캐싱했던 데이터를 재사용합니다.

     

    테스트 결과 만약 가상 사용자가 100명이라고 했을 때 기존 Portfolly 페이지는 포트폴리오 5개 섹션을 왕복 순회했을 때 최대 3.494초의 서버 지연을 불러일으키는데,

     

    리팩터링한 Portfolly 페이지는 왕복 순회 시 최대 1.364초의 지연만 발생해서 1/3정도 감소했다고 볼 수 있습니다!

     

     

Designed by Tistory.