React - Data Caching

sarang_daddy·2023년 9월 24일
1

React

목록 보기
20/26
post-thumbnail

Data Caching?

Data Caching이란 반복적으로 엑세스되는 데이터를 빠르게 사용할 수 있도록 메모리에 일시적으로 저장하는 프로세스를 의미한다.

데이터 캐싱으로 얻을 수 있는 이점이 많기에 성능 향상과 효율적인 리소스 사용을 위해 필수적으로 사용되고 있다.

데이터 캐싱의 이점

  • 속도 향상
    : 캐시 메모리는 RAM과 같은 저장소에 위치하므로 외부 서버에 접근하는 것보다 빠르게 엑세스할 수 있다.

  • 부하 감소
    : 반복적으로 요청되는 데이터를 캐시에 저장함으로써 서버로의 데이터 요청 수를 줄일 수 있다.

  • 사용자 접근성 향상
    : 캐시된 데이터로 빠르게 화면에 사용자가 원하는 데이터를 보여줄 수 있다.

데이터 캐싱이 없다면 아래 화면처럼 페이지가 렌더링 될 때마다 데이터를 서버로 부터 가져 오기에 loading 메시지가 반복된다.

React Query라는 라이브러리에서는 데이터 캐싱을 자동으로 지원해주지만,
이번에는 Context API를 사용하여 데이터 캐싱 로직을 직접 구현해보자.

Data Caching 구현

데이터 캐싱 구현에는 몇 가지 주의 사항이 있다.

  • 캐시 일관성
    : 캐시된 데이터는 원본 데이터와 동기화 되어야 한다.
    즉, 캐시된 데이터가 오래되거나 변경되면, 업데이트 혹은 무효화해야 한다.

  • 메모리 사용
    : 캐싱은 추가적인 메모리를 사용하므로, 캐시 크기와 관리 전략에 대한 고려가 필요하다.

  • 캐시 전략
    : 어떤 데이터를 캐시에 저장할지, 얼마나 오래 저장할지, 언제 캐시를 무효화 할지 등의 정의가 필요하다.

이번 구현에는 만료 시간을 할당하여 캐시의 유효성을 판단하도록 했다.

1. 캐싱 메니저 함수 생성

  • 캐싱 로직을 관리하는 헬퍼 함수를 생성한다.
  • 이 함수는 캐싱할 데이터와 해당 데이터의 만료 시간을 관리한다.
  • 즉, 캐시된 데이터의 유효성을 검사하고 데이터를 반환한다.
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;

  • ONE_MINUTE_MS : 1분을 밀리초로 표현한 상수.
  • cacheManager : 캐싱 로직을 관리하는 함수. 캐시 만료 시간은 10분으로 지정.
  • cacheDataisDataValid 두 메서드를 포함하는 객체를 반환한다.
  • 외부에서 cacheManager를 통해 캐시된 데이터를 가져오거나 새로운 데이터를 저장하고, 유효성 검사가 가능하다.

  • 캐시를 저장할 객체.
  • 각 키는 문자열이며, 값은 { data: any; expireTime: number } 형태를 가진다.
  • 여기서 data는 캐싱할 데이터를 나타내고, expireTime은 해당 데이터의 만료 시간을 나타낸다.

  • 주어진 키에 해당하는 데이터를 캐시에서 가져오거나 새로운 데이터를 캐시에 저장하는 함수.
  • 주어진 키가 존재하고 만료 시간이 현재 시간보다 미래라면 캐시된 데이터를 반환한다.
  • 주어진 키가 없거나 만료된 경우, 새로운 데이터와 만료 시간을 저장한다.
  • 캐시된 데이터 혹은 새로 캐시된 데이터를 반환한다.

  • 주어진 키의 캐시 데이터의 유효성을 검사하는 함수.
  • 주어진 키에 해당하는 데이터가 캐시에 있고 그 데이터의 만료 시간이 아직 지나지 않았다면 true를 반환한다.
  • 그렇지 않은 경우 false를 반환한다.

2. CacheContext 생성

  • 앱 전체에서 데이터 캐싱을 관리하기 위해 Context API로 CacheContext를 생성한다.
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);

  • cacheManager에서 반환되는 두 메서드를 사용할 수 있는 context를 생성한다.

  • contextProvider를 사용하여 cacheData, isDataValid를 제공한다.
  • contextProvider의 하위 컴포넌트들은 cacheData, isDataValid 함수에 접근할 수 있다.

  • 함위 컴포넌트에서 context를 사용할 useContext를 커스텀 훅으로 만들어준다.
  • 각 컴포넌트에서 useContext를 import하고 호출할 필요가 없어진다.
  • useCacheContext는 CacheContext의 값을 가져와서 반환하는 함수(훅)이다.
  • 이 함수의 반환 값의 타입은 ICacheContext 타입이다.

3. useFetch 훅 작성

  • 데이터 패치와 캐싱 로직을 관리하는 useFetch 훅을 작성한다.
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;
};

  • Status는 데이터 가져오기 상태를 나타내는 문자열 유니언 타입이다.
  • UseFetch는 useFetch 훅이 반환하는 객체의 타입이다.
  • FetchOptions는 useFetch 훅에 전달되는 인자의 타입이다.
// 기존 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,
  });
  • useFetch 커스텀훅을 사용하는 컴포넌트에서 useFetch 인자를 객체로 전달하도록 수정.
  • ...rest는 마지막 인자로 전달되어야 하기에 순서를 고려하지 않고 전달하기 위해 수정했다.

함수에 전달되는 모든 인자들을 객체의 속성으로 묶어서 전달하면, 순서에 구애받지 않고 인자들을 함수에 전달할 수 있다.

  • useFetch 함수에 전달되는 인자 정의.
  • cacheKey가 추가되었다.
// 수정 전 useFetch 인자 정의
export const useFetch = <T>(
  fetchFunction: (...args: any[]) => Promise<T>,
  ...args: any[]
): UseFetch<T> => {

  • state와 setState는 useFetch 훅의 내부 상태를 관리한다.
  • cacheData와 isDataValid 함수는 캐시 컨텍스트에서 가져온다.
  • 이 함수들은 데이터를 캐시에 저장하고, 캐시된 데이터의 유효성을 확인하는데 사용된다.

  • useEffect를 사용하여 API에서 데이터를 가져온다.

  • 초기 상태(initial) 확인하여 캐시에서 유요한 데이터가 있는지 확인.
  • 유효한 데이터가 있으면 해당 데이터로 상태를 업데이트 한다.
  • 유요한 데이터가 없다면 fetchData 함수를 호출하여 API 데이터를 가져온다.

  • 클린업 함수로 언마운트 됬을 경우 네트워크 요청으로 인한 상태 변경을 방지한다.
  • useEffect의 의존성 배열을 정의한다.
  • useFetch 훅은 현재 상태를 반환한다. 이 상태는 데이터 가져오기의 결과 및 상태 정보를 포함한다.

4. 앱 구조 작성 (App)

  • 앱의 최상위 컴포넌트에서 CacheContextProvider를 사용하여 캐싱 관련 함수를 앱 전체에 제공한다.

Data Caching 결과

  • API로부터 데이터를 받는 동안 화면에 보이면 "Loading..."이 동일 페이지 이동시 보이지 않는다.
  • 한번 가져온 데이터를 메모리에 캐싱하여 동일한 데이터가 필요한 경우 API 요청이 생략되었다.

데이터 캐싱 매니저 함수와 Context API를 사용한 로직을 useFetch 훅에 적용하여 API에서 데이터를 가져오는 기능과 함께 데이터 캐싱 기능이 가능하도록 구현되었다.

React Query를 사용하면 기본으로 제공되는 기능이지만, 기본 원리를 이해하는 것은 라이브러리를 더욱 효율적으로 사용하고 문제가 발생 했을때 디버깅에도 용이하다고 생각한다.

profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글