Data Caching
이란 반복적으로 엑세스되는 데이터를 빠르게 사용할 수 있도록 메모리에 일시적
으로 저장하는 프로세스를 의미한다.
데이터 캐싱으로 얻을 수 있는 이점이 많기에 성능 향상과 효율적인 리소스 사용을 위해 필수적으로 사용되고 있다.
속도 향상
: 캐시 메모리는 RAM과 같은 저장소에 위치하므로 외부 서버에 접근하는 것보다 빠르게 엑세스할 수 있다.
부하 감소
: 반복적으로 요청되는 데이터를 캐시에 저장함으로써 서버로의 데이터 요청 수를 줄일 수 있다.
사용자 접근성 향상
: 캐시된 데이터로 빠르게 화면에 사용자가 원하는 데이터를 보여줄 수 있다.
데이터 캐싱이 없다면 아래 화면처럼 페이지가 렌더링 될 때마다 데이터를 서버로 부터 가져 오기에 loading 메시지가 반복된다.
React Query
라는 라이브러리에서는 데이터 캐싱을 자동으로 지원해주지만,
이번에는 Context API
를 사용하여 데이터 캐싱 로직을 직접 구현해보자.
데이터 캐싱 구현에는 몇 가지 주의 사항이 있다.
캐시 일관성
: 캐시된 데이터는 원본 데이터와 동기화 되어야 한다.
즉, 캐시된 데이터가 오래되거나 변경되면, 업데이트 혹은 무효화해야 한다.
메모리 사용
: 캐싱은 추가적인 메모리를 사용하므로, 캐시 크기와 관리 전략에 대한 고려가 필요하다.
캐시 전략
: 어떤 데이터를 캐시에 저장할지, 얼마나 오래 저장할지, 언제 캐시를 무효화 할지 등의 정의가 필요하다.
이번 구현에는 만료 시간을 할당하여 캐시의 유효성을 판단하도록 했다.
const ONE_MINUTE_MS = 60 * 1000;
const cacheManager = (cacheExpirationDuration: number = ONE_MINUTE_MS * 10) => {
const cache: Record<string, { data: any; expireTime: number }> = {};
return {
cacheData: (key: string, data?: any) => {
if (cache[key]) {
const { data: cachedData, expireTime } = cache[key];
if (expireTime > Date.now()) {
return cachedData;
}
}
cache[key] = { data, expireTime: Date.now() + cacheExpirationDuration };
return data;
},
isDataValid: (key: string) => {
if (!cache[key]) return false;
const { expireTime } = cache[key];
return expireTime > Date.now();
},
};
};
export default cacheManager;
cacheData
와 isDataValid
두 메서드를 포함하는 객체를 반환한다.import { createContext, useContext } from 'react';
import cacheManager from '@/helpers/cacheManager';
interface ICacheContext {
cacheData: (key: string, data?: any) => any;
isDataValid: (key: string) => boolean;
}
export const CacheContext = createContext<ICacheContext>({} as ICacheContext);
interface CacheContextProviderProps {
children: React.ReactNode;
}
export const CacheContextProvider = ({
children,
}: CacheContextProviderProps) => {
const { cacheData, isDataValid } = cacheManager();
return (
<CacheContext.Provider value={{ cacheData, isDataValid }}>
{children}
</CacheContext.Provider>
);
};
export const useCacheContext: () => ICacheContext = () =>
useContext(CacheContext);
import { useState, useEffect } from 'react';
import { useCacheContext } from '@/contexts/CacheContext';
type Status = 'initial' | 'pending' | 'fulfilled' | 'rejected';
interface UseFetch<T> {
data?: T;
status: Status;
error?: Error;
cacheKey: string;
}
interface FetchOptions<T> {
fetchFunction: (...args: any[]) => Promise<T>;
args: any[];
cacheKey: string;
}
export const useFetch = <T>({
fetchFunction,
args,
cacheKey,
}: FetchOptions<T>): UseFetch<T> => {
const [state, setState] = useState<UseFetch<T>>({
status: 'initial',
data: undefined,
error: undefined,
cacheKey,
});
const { cacheData, isDataValid } = useCacheContext();
useEffect(() => {
let ignore = false;
const fetchData = async () => {
if (ignore) return;
setState((state) => ({ ...state, status: 'pending' }));
try {
const response = await fetchFunction(...args);
cacheData(cacheKey, response);
setState((state) => ({
...state,
status: 'fulfilled',
data: response,
cacheKey,
}));
} catch (error) {
setState((state) => ({
...state,
status: 'rejected',
error: error as Error,
cacheKey,
}));
}
};
if (state.status === 'initial') {
if (isDataValid(cacheKey)) {
setState((state) => ({
...state,
status: 'fulfilled',
data: cacheData(cacheKey),
cacheKey,
}));
} else {
fetchData();
}
}
return () => {
ignore = true;
};
}, [fetchFunction, cacheKey, cacheData, isDataValid, state.status]);
return state;
};
// 기존 Home 컴포넌트
const { data: characters, status } = useFetch(fetchCharacters, 50);
// 수정된 Home 컴포넌트
export const Home = () => {
const { data: characters, status } = useFetch({
fetchFunction: fetchCharacters,
args: [50],
cacheKey: ROUTE_PATH.HOME,
});
...rest
는 마지막 인자로 전달되어야 하기에 순서를 고려하지 않고 전달하기 위해 수정했다.함수에 전달되는 모든 인자들을 객체의 속성으로 묶어서 전달하면, 순서에 구애받지 않고 인자들을 함수에 전달할 수 있다.
// 수정 전 useFetch 인자 정의
export const useFetch = <T>(
fetchFunction: (...args: any[]) => Promise<T>,
...args: any[]
): UseFetch<T> => {
데이터 캐싱 매니저 함수와 Context API를 사용한 로직을 useFetch 훅에 적용하여 API에서 데이터를 가져오는 기능과 함께 데이터 캐싱 기능이 가능하도록 구현되었다.
React Query를 사용하면 기본으로 제공되는 기능이지만, 기본 원리를 이해하는 것은 라이브러리를 더욱 효율적으로 사용하고 문제가 발생 했을때 디버깅에도 용이하다고 생각한다.
글 잘 읽었습니다!!