prefetchQuery를 통한 프론트엔드 성능 개선

Allen (김흥수)
한국신용데이터 팀 블로그
6 min readAug 9, 2023

--

안녕하세요. 한국신용데이터 관리서비스팀 Allen(김흥수) 입니다.

유난히도 느리던 페이지의 로딩 속도를 1000%(단순 계산으로) 개선한 사례가 있어 소개드리고자 합니다. 기존에 작성된 코드의 수정 없이, 약간의 코드를 추가하여 개선했던 사례라 도움이 되신다면 좋겠습니다.

(참고로 아래 내용은 react 18 버전과 react-query를 활용한 CSR환경을 가정하여 작성되었습니다.)

문제 인식

캐시노트의 매출 장부 서비스에는 비교 및 분석이라는 페이지가 존재합니다.
이 페이지는 7개월치 월별 매출, 1개월전 동기간 매출, 1년전 동기간 매출, 최근 3개월 평균 매출 등 여러 데이터를 이용해 매출 현황의 비교와 분석을 도와주는 페이지입니다.

비교 및 분석 페이지
비교 및 분석 페이지의 일부

이 페이지는 다른 페이지에 비해 유난히 느리다는 의견이 많았습니다. 사용하는 api가 많기도 했지만, 5초 이상 기다려야 화면이 뜨는 경우도 잦았습니다. 이건 아무래도 너무 느린 것이기에 원인을 파악하기로 했습니다.

원인 파악

문제의 원인은 생각보다 쉽게 파악할 수 있었습니다. 네트워크 탭을 살펴보니 api가 계단식으로 순차 호출되는 것을 확인할 수 있었습니다. 여기서 뭔가 이상함을 직감하였습니다.

순차적으로 호출되고 있는 api

해당 코드를 살펴보니 조회 기간에 따라 9~11개의 api를 하나의 컴포넌트에서 호출하고 있었습니다.
단순하게 표현하면 아래와 같은 상태였습니다.

// 하나의 컴포넌트에서 여러 api를 호출
function MonthlyAnalysis() {
useDataA()
useDataB()
useDataC()
...
}

function Page() {
return (
<Suspense fallback={<Loader />}>
<MonthlyAnalysis />
...
</Suspense>
)
}

react-query의 useQuery 함수는 기본적으로 여러개를 나열하면 동시에 병렬로 호출합니다. 하지만 suspense mode를 사용하는 경우 그렇지 않습니다. 첫번째 useQuery함수가 Suspense로 Promise를 던지고, 해당 Promise가 fulfilled되기 전까지, 이후의 api fetching을 blocking하기 때문입니다.

순차 호출

따라서 react-query 공식 문서에서는 useQuery 대신 useQueries 를 통해 병렬호출하도록 권장하고 있습니다. (참고: Parallel Queries)

When using React Query in suspense mode, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you’ll either need to use the useQueries hook (which is suggested) or orchestrate your own parallelism with separate components for each useQuery instance (which is lame).

문제 해결

여러개의 useQuery를 useQueries로 바꾸면 해결할 수 있다는 사실을 알게 되었습니다.

하지만 사실 다른 방법이 하나 더 존재합니다.

바로 prefetchQuery를 활용하는 것입니다.

prefetchQuery는 suspense와 무관하게 동작합니다. 때문에 여러 api를 동시에 병렬 호출 할 수 있습니다. 그리고 useQuery는 prefetch 중인 api는 중복호출하지 않습니다. 즉, prefetch 한 api는 useQuery에 의해 suspend 되지 않습니다.

게다가 prefetchQuery를 활용하면, 기존 코드를 수정하지 않아도 됩니다.

순차 호출하던 api를 prefetch 하는 코드만 추가하면, 기존 코드를 수정하지 않으면서 api를 동시에 병렬로 호출 할 수 있습니다.

이는 페이지를 렌더링할 때, suspense 밖에서 prefetchQuery를 호출하는 형태로 구현할 수 있습니다. (실제 구현된 코드는 이와 조금 다르지만 여기서는 간단히 소개합니다.)

function usePrefetchData() {
useEffect(() => {
prefetchDataA()
prefetchDataB()
prefetchDataC()
...
}, []
}

function Page() {
usePrefetchData()

return (
<Suspense fallback={<Loader />}>
<MonthlyAnalysis />
</Suspense>
)
}
병렬 호출

이 내용은 사실 Render-as-you-fetch와 동일한 개념입니다. Render-as-you-fetch 방식을 잘 지킨다면 얻을 수 있는 효과이기도 합니다.

후기

비교 및 분석 페이지에 이 내용을 적용한 결과, 로딩 속도가 1/10정도로 줄었습니다. 11개의 api를 순차적으로 호출하던 것을 한번에 병렬 호출하게 된 덕분입니다.

실측 기록이 없어 정확한 값을 소개해드리지 못하는게 아쉽습니다. 글을 쓰기 위해 작업했던 내용이 아니라서요. 다만 제일 느리던 페이지가 제일 빨라졌다며 팀원 모두 기뻐했던 기억이 있네요 :)

개인적으로도 적은 노력으로 큰 효과를 얻었던 작업이라 기억에 남습니다. 시간은 항상 제한적이기 때문에 좋은 서비스를 만들기 위해 다양한 시도와 고민을 하게 되는 것 같습니다.

그럼 또 소개드릴만한 내용으로 찾아뵙겠습니다. 😃

--

--