react query를 이용한 캐싱 - boostcampwm-2022/web33-Mildo GitHub Wiki

리액트 쿼리를 도입하려는 이유

왜 캐싱하려 하는가?

Day21 - 캐싱 전.gif

  • 마커를 클릭하면 모달창이 표시된다. 모달창은 1단계, 2단계로 나뉜다. 2단계 모달창에서 해당 지역의 24시간 이내의 인구 밀도 정보를 그래프로 보여줘야 한다.
  • 24시간 이내의 인구 밀도 정보는 2단계 모달창을 열 때마다 서버에 요청된다. 이 응답은 약 1.2~1.8초가 걸린다.
  • 즉, 2단계 모달창을 열 때마다 1.2초~1.8초를 기다려야 한다.
  • 24시간 이내의 인구 밀도 정보는 30분 간격으로 보여주기 때문에 실시간성이 크게 보장되지 않아도 된다.
  • 그렇다면 한 번 요청한 데이터는 몇 분 정도 지나도 같은 데이터를 보여줘도 되지 않을까?
  • 그래서 jotai를 이용해 한 번 응답 받은 데이터는 전역 상태로 저장하고, 이후 같은 지역에 대한 데이터를 보여줘야 할 때에는 전역 상태에서 가져오도록 했다.

왜 리액트 쿼리를 적용하려 하는가?

  • jotai를 이용해 전역 상태로 관리하면 캐싱 효과를 구현할 수 있었지만, 한 번 전역 상태로 저장된 데이터는 이후 변경되지 않는다.

  • 즉, 사용자가 새로고침 하지 않으면 아무리 시간이 지나도 최초로 모달창을 열었던 그 시간을 기준으로 24시간 이내의 인구 밀도 정보를 보여준다.

    ex) 오후 2시에 처음 모달창을 열고 새로고침을 하지 않은 상태에서 오후 9시에 모달창을 열게 되면, 오후 2시를 기준으로 24시간 이내의 인구 밀도 정보 가 표시된다.

  • 좀 더 본격적인 캐싱 방법을 찾아보던 도중 리액트 쿼리로 캐싱을 구현할 수 있다는 것을 알게 되었고, 학습에도 도움이 될 것이라 생각하여 리액트 쿼리를 적용하기로 했다.

구현 방법과 학습 내용

이전 코드

...

const [secondLevelInfoCache, setSecondLevelInfoCache] = useAtom(
  secondLevelInfoCacheAtom
);
const [graphInfo, setGraphInfo] = useState<SecondLevelTimeInfoCacheTypes>({});

...

// 전역에 areaName을 키로 갖고 있는 속성이 있으면 hit
if (secondLevelInfoCache[areaName]) {
  setGraphInfo(secondLevelInfoCache[areaName]);
  return;
}

// 아니면 api 호출
const { data } = await apis.getPastInformation(areaName);

// 전역 상태로 저장하고
setSecondLevelInfoCache({ ...secondLevelInfoCache, [areaName]: data });
// 그래프 보여주기
setGraphInfo(data);
  1. 해당 지역의 24시간 이내의 인구 밀도 정보 가 전역 상태(secondLevelInfoCache)에 저장되어 있으면 hit
  2. 없으면 api 서버에 요청하고 전역 상태에 저장

리액트 쿼리 학습하기

[React Query](https://www.notion.so/React-Query-18849fd1e55e43fa97e6838291dfa92b)

요약

  • react query
    • 서버와 클라이언트가 비동기적으로 공유하는 데이터인 ‘서버 상태’를 관리하는 관리하는 라이브러리이다.
    • 데이터 캐싱이 가능하다.
    • 데이터를 불러오는 로직과 state로 설정하는 로직이 합쳐져서 좀 더 깔끔한 코드를 작성할 수 있다.
    • suspense: true 만 추가하면 간단하게 susepnse 기능을 구현할 수 있다.
  • useQuery
    • 서버에서 데이터를 가져오고 캐싱하는 훅이다.
    • query key, query function, options를 인자로 받는다.

캐싱을 위한 조건

options 설정이 중요하다.

  • staleTime을 설정해줘야 한다.
    • 0이면 캐시된 데이터는 항상 stale하기 때문에 refetching하게 되어 서버에 계속 요청한다.
  • staleTime보다 cacheTime이 더 길어야 한다.
    • cacheTime이 더 짧으면 데이터가 상하지 않았지만 캐시에 있는 데이터는 이미 가비지 컬렉터에 의해 처리되었으므로 캐싱 기능을 이용할 수 없다.
  • enabled 옵션 설정
    • true이면 계속 요청을 보내고 false이면 캐싱 기능을 사용하지 않는 것과 마찬가지이기 때문에 보통 조건문으로 결정한다.

리액트 쿼리 적용 후

// hooks/useGraphInfo.tsx
const { data: graphInfoResponse } = useQuery(
    ['getGraphInfo', firstLevelInfo ? firstLevelInfo[0] : ''], // query key
    async () => { // query function
      if (!firstLevelInfo) {
        return null;
      }
      const result = await apis.getPastInformation(firstLevelInfo[0]);

      return result;
    },
    {
      enabled,
      staleTime: QUERY_TIME.STALE_TIME, // 5분
      cacheTime: QUERY_TIME.CACHE_TIME, // 30분
      onSuccess: data => {
        success(data);
      },
      onError: e => {
        console.log('error', e);
      }
    }
  );
	return [graphInfoResponse];
};

export default useGraphInfo;
  1. query key

    • 지역에 따라 24시간 이내의 인구 밀도 정보 를 요청하므로 1번째 인덱스에 지역 이름을 붙여주었다.

      ex) [’getGraphInfo’, ‘서울역’]

  2. query function

    • 모달창을 열지 않았을 때는 null을 return하고 그 외에는 서버에 요청한다.
  3. query options

    • enabled : 특정 상황에만 query function이 실행되도록 설정(후에 추가 설명)했다.
    • staleTime : 캐시 데이터가 언제 상하게(?) 되는지 결정, 5분으로 설정했다.
    • cacheTime : 캐시된 데이터가 30분 동안 메모리에 저장되는 것으로 설정했다.
    • onSuccess : 요청에 성공 시 데이터로 그래프를 그려준다.
  • enabled

    const enabled = () => {
    	// 1.
      if (!isSecondLevel) {
        return false;
      }
    	// 2.
      if (!firstLevelInfo || !prevFirstLevelInfo) {
        return true;
      }
    	// 3.
      if (prevFirstLevelInfo[0] === firstLevelInfo[0]) {
        return false;
      }
    	// 4.
      return true;
    };
    1. 2단계 모달창이 열리지 않았을 때는 요청하지 않는다.

    2. 이전에 2단계 모달창이 한 번도 열리지 않았을 때, 즉 처음으로 2단계 모달창이 열릴 때는 요청한다.

    3. 이전에 선택됐던 지역과 같은 지역을 선택했을 때는 요청하지 않는다.

      ex) 서울역 마커를 누르고, 2단계 모달창을 닫고, 다시 서울역 마커를 눌렀을 때

    4. 이전에 선택됐던 지역과 다른 지역을 선택했을 때는 요청한다.

      ex) 서울역 마커를 누르고, 명동 마커를 눌렀을 때

  • 그래프 적용

    // components/SecondLevelComponent/SecondLevelComponent.tsx
    
    // 1.
    const success = (data: graphInfoResponseTypes | null) => {
      if (data) {
        setGraphInfo(data.data);
        setPrevFirstLevelInfo(firstLevelInfo);
      }
    };
    
    const setPastInformation = async (): Promise<undefined> => {
      if (!firstLevelInfo) {
        return;
      }
    
      if (graphInfoResponse) {
        setGraphInfo(graphInfoResponse.data);
        setPrevFirstLevelInfo(firstLevelInfo);
      }
    
      // eslint-disable-next-line no-useless-return
      return;
    };
    
    // 2.
    useEffect(() => {
      if (!isSecondLevel) {
        ...
      }
    
      setPastInformation();
    }, [isSecondLevel]);
    1. 요청을 성공했을 땐 success 함수가 실행된다.
    2. 이미 캐싱된 데이터를 가져올 땐 setPastInformation 함수가 실행된다!

결과

Day21 - 캐싱 후.gif

  • 서울역을 다시 열었을 때 그래프 그려지는 속도가 매우 빨라짐

Day21 - 캐싱 후4.gif

  • 한 번 요청한 데이터(서울역)는 다시 요청되지 않음
⚠️ **GitHub.com Fallback** ⚠️