현재 개발 중인 앱에서는 로그인 후 메인 화면에 들어가면, 서버로부터 해당 달에 해당하는 모든 정보를 불러와 사용자에게 제공한다. 그러나, 달별, 일별로 각각 데이터가 있는 바람에 앱의 성능이 좋지 않았다. 메인 화면에 들어갔을 때, 서버에서 데이터를 가져오는 시간 + 이미지와 데이터를 불러오는 시간 때문에 앱이 동작하지 않는 것처럼 10초 정도 멈춰있었다. 그래서 로딩 이펙트나 skeleton UI를 사용해서 실제 로딩 시간보다 사용자가 느끼는 체감 로딩 시간을 줄이려고 했다. 로딩이 된다는 사실을 사용자에게 알리는 건 좋지만, 오히려 로딩 시간이 실제보다 더 길게 느껴지는 단점이 있었다.
오히려, 사용자 체감 시간을 줄이는 것보다도 물리적 시간을 줄여보자는 생각으로 앱의 전반적 성능을 개선하기 위해서 다양한 방법을 고려했다.
1. 특정 데이터가 바뀔 때만 API 호출하기(useEffect)
캘린더 데이터를 받아오기 위해서 처음에 마운트되거나 달력을 넘기는 경우에 새로운 달에 대한 데이터를 가져오도록 useEffect()를 사용했다. 그런데 useEffect()의 성능을 개선할 수 있지 않을까 하고 고민하던 중에 fetchCalendar() 자체를 캐싱하면 더 빠르지 않을까 결론을 내렸다.
// * 컴포넌트가 마운트 되면 데이터를 새로 불러옴
useEffect(() => {
fetchCalendar();
}, []);
// * 달력 페이지를 넘기는 경우 데이터를 새로 불러옴
useEffect(() => {
fetchCalendar();
}, [currentMonth]);
2. useFocusEffect와 useCallback
useCallback으로 함수를 캐싱하고 useFocusEffect으로 해당 페이지가 포커싱될 때만 작동하도록 설계하였다. 기존에 평균 8초 정도 걸리던 랜더링 시간이 6초로 줄어들긴 했지만, 조금 더 개선할 수 있을 것이라 생각했다. 그래서 API 자체를 캐싱하면 UI 렌더링 시간은 줄이지 못하더라도, API 요청부터 응답까지의 시간을 줄일 수 있을 거라 생각을 했다.
// * 컴포넌트가 마운트 되면 데이터를 새로 불러옴
useEffect(() => {
fetchCalendar();
}, []);
// * 달력 페이지를 넘기는 경우 데이터를 새로 불러옴
useFocusEffect(
useCallback(() => {
fetchCalendar();
}, [currentMonth])
);
import React, {useCallback, useState} from 'react';
import {useSelector} from 'react-redux';
import {RootState} from '../../settings/store';
import AxiosClient from '../../utils/FetchClient';
export const useGolfCalendar = (currentMonth: Date) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [calendarData, setCalendarData] = useState<any[]>([]);
const [events, setEvents] = useState<any[]>([]);
const golfFieldId = useSelector((state: RootState) => state.user.golfFieldId);
const axiosClient = new AxiosClient();
const fetchCalendar = useCallback(async () => {
setIsLoading(true);
try {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
const data = await axiosClient.get<any[]>(
`/reservation-sheet/calendar/${golfFieldId}/${year}/${month}`
);
setCalendarData(data);
const newEvents = data.flatMap((item: any) => [
{
id: 1,
start: new Date(item.targetDate),
end: new Date(item.targetDate),
icon: 'alarm',
title: item.totalCntSum,
totalCntSum: item.totalCntSum,
},
{
id: 2,
start: new Date(item.targetDate),
end: new Date(item.targetDate),
icon: 'person',
title: item.availableCntSum,
availableCntSum: item.availableCntSum,
},
{
id: 3,
start: new Date(item.targetDate),
end: new Date(item.targetDate),
icon: 'lock',
title: item.blockedCntSum,
blockedCntSum: item.blockedCntSum,
},
]);
setEvents(newEvents);
} catch (error) {
console.error('Error fetching Calendar data:', error);
} finally {
setIsLoading(false);
}
}, [currentMonth, golfFieldId, axiosClient]);
return {isLoading, calendarData, events, fetchCalendar};
};
3. useQuery 사용
API 캐싱을 위해서 useQuery()를 사용했다.
import * as React from 'react';
import Router from './src/settings/Router';
import {QueryClient, QueryClientProvider} from 'react-query';
import {SafeAreaView} from 'react-native';
import SplashScreen from 'react-native-splash-screen';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
cacheTime: 1000 * 60 * 5,
},
},
});
export default function App() {
React.useEffect(() => {
setTimeout(() => {
SplashScreen.hide();
}, 1000);
});
return (
<SafeAreaView style={{flex: 1}}>
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
</SafeAreaView>
);
}
먼저 queryClient를 선언하고 <QueryClientProvider>로 App.tsx 전체를 묶어줘야 한다.
const {
isLoading,
events,
error,
refetch: refetchCalendar,
} = type === 'Golf'
? useGolfCalendar(currentMonth)
: useCaddyCalendar(currentMonth);
const fetchCalendarData = useCallback(() => {
refetchCalendar();
}, [refetchCalendar]);
useFocusEffect(
useCallback(() => {
const newDate = new Date();
setCurrentMonth(newDate);
fetchCalendarData();
}, [fetchCalendarData])
);
useQuery()를 사용하면서 궁금했던 부분은 useCallback()으로 함수도 캐싱하고 Query로 API 캐싱까지 하면 성능이 더 개선되지 않을까? 였다.
결론부터 말하자면 성능면에서 큰 차이는 없다. useCallback()은 불필요한 리렌더링을 줄이는데 목적이 있고, react-query는 네트워크 캐싱을 통해 불필요한 API 요청을 줄이는 데 목적이 있다. 간단하게 말하면 react-query는 stale-while-revalidate 전략을 사용해서 캐시된 데이터를 즉시 반환하고 백그라운드에서 새 데이터를 요청하는 방식이다. 그래서 UI는 빠르게 반응하면서도 데이터는 최신 상태로 유지되는 장점이 있다.
정리하면, react-query에서 data fetching을 효율적으로 관리하고 있기 때문에 굳이 useCallback으로 함수를 캐싱하는 일은 불필요하다는 것이다.
import React, {useCallback, useState} from 'react';
import {useSelector} from 'react-redux';
import {useQuery} from 'react-query';
import {RootState} from '../../settings/store';
import AxiosClient from '../../utils/FetchClient';
export const useGolfCalendar = (currentMonth: Date) => {
const golfFieldId = useSelector((state: RootState) => state.user.golfFieldId);
const axiosClient = new AxiosClient();
const fetchCalendar = async () => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
const data = await axiosClient.get<any[]>(
`/reservation-sheet/calendar/${golfFieldId}/${year}/${month}`
);
return data.flatMap((item: any) => [
{
id: 1,
start: new Date(item.targetDate),
end: new Date(item.targetDate),
icon: 'alarm',
title: item.totalCntSum,
totalCntSum: item.totalCntSum,
dateStatus: item.dateStatus,
},
{
id: 2,
start: new Date(item.targetDate),
end: new Date(item.targetDate),
icon: 'person',
title: item.availableCntSum,
availableCntSum: item.availableCntSum,
dateStatus: item.dateStatus,
},
{
id: 3,
start: new Date(item.targetDate),
end: new Date(item.targetDate),
icon: 'lock',
title: item.blockedCntSum,
blockedCntSum: item.blockedCntSum,
dateStatus: item.dateStatus,
},
]);
};
const {
isLoading,
data: events = [],
error,
refetch,
} = useQuery(['golfCalendar', currentMonth], fetchCalendar);
return {isLoading, events, error, refetch};
};
결론
useEffect() -> useCallback() -> useQuery()로 바꾸면서 네트워크 지연 시간, UI 렌더링 시간을 포함한 대기 시간이 11초 가량 소요되었는데, useQuery()를 사용해서 5초로 줄일 수 있었다. useQuery이 제공하는 전역 상태 관리, 데이터 페칭 등 다양한 기능을 앱 내에 사용해보고자 한다.
reference
'Web > React-native' 카테고리의 다른 글
Axios 인터셉터로 JWT 로테이션 구현하기 (0) | 2024.08.22 |
---|---|
중첩 네비게이션에서 발생하는 Stack 초기화 문제 해결(feat.CommonAcitons) (0) | 2024.08.22 |
스켈레톤 화면이 사용자 경험 개선에 도움이 될까 (0) | 2024.08.04 |