-
[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}§ion=${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부터 chacheTime이 gcTime으로 바뀌었습니다. 바뀐 다른 내용은 아래 블로그를 참고하세요)
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를 배열로 관리하는 이유와 방법에 대한 더 자세한 내용은 아래 블로그를 참고하면 좋습니다.
queryFn 지정하기
queryFn은 Promise 객체를 반환하는 함수입니다. 서버에 비동기 데이터를 요청하는 함수를 할당하면 useQuery가 데이터를 재요청 할 때마다 해당 함수를 호출합니다.
저의 경우 다음과 같은 함수를 지정해줬습니다.
const getPortfolios = ({ pageParam }: { pageParam: number }) => { return fetch(`/portfolios?page=${pageParam}§ion=${section}&${filterQueryString}`, 'GET'); };
fetch는 api를 요청하는 로직을 제가 유틸 함수로 분리시켜 사용하는 함수입니다.
방법은 다른 게시물에 기록했습니다.
카테고리, 태그, 키워드 별 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정도 감소했다고 볼 수 있습니다!
'Studying > Proj 과정' 카테고리의 다른 글
[Portfolly] 직접 반응형 카테고리 Slider 만들기 (0) 2023.12.24 [Portfolly] 메인 페이지 UI/UX를 개선해보자(feat. mobbin) (0) 2023.12.24 [Portfolly] 메인 페이지 Lighthouse 분석 (0) 2023.08.01 [Portfolly] 프로젝트를 마치며 (0) 2023.07.27 [Portfolly] 코드 Depth를 최대한 줄여보자 (0) 2023.07.27