[Recoil] Selector를 이용하여 API 값 캐싱하기 with React & TypeScript 📮

yiyb0603·2021년 5월 13일
15

React

목록 보기
14/15
post-thumbnail

안녕하세요! 오늘은 TypeScript + React 환경에서 Recoil 전역 상태관리의 기능중 하나인 Selector를 이용하여 Axios의 데이터 값을 캐싱하는 방법에 대해서 알아보도록 하겠습니다.

아래는 이전에 Recoil 상태관리 라이브러리에 대해서 작성한 글입니다.
https://vo.la/ZeOOK

1. Selector를 사용하기 전 문제점 😪😪

이번에 Selector에 대해서 제대로 알아보기 전, 저는 Recoil의 기능중 하나인 atom만을 사용하여 Custom Hook과 함께 전역 상태관리를 진행했습니다. 그저 기능은 순수하게 잘 작동하였지만 성능은 좋지 않았습니다. 그 이유는 바로 API가 호출된 값이 캐싱이 되지 않았었기 때문입니다.

포스트 탭을 전환할때마다 API가 무분별하게 호출됨.

거의 바뀌지 않는 데이터를 무분별하게 반복적으로 호출하게 된다면 좋지 않다고 생각이 들어서 개선할 방법을 생각하게 되었습니다. 🤔🤔

직접 API를 캐싱하여 성능을 개선하는 방법도 있었지만, atom과 연관된 Recoil 내에서의 방법이 있을 것 같아서 직접 찾아보았고, 평소에 큰 관심이 없었던 Selector의 기능을 알게되고난 후, 즉시 Selector를 도입해야겠다고 생각했습니다.

2. Selector를 도입해본 이유 🎆

기본적으로, selector 를 이용하여 비동기 서버 통신시 얻을 수 있는 가장 큰 장점중, 자체적으로 캐싱을 지원하기 때문에 같은 입력값에 있어서 이전에 캐싱된 결과를 바로 보여주고, 퍼포먼스 면에서도 훨씬 유리한 장점이 존재합니다. 😁

즉, Selector의 캐싱을 이용하여 위의 이슈를 해결 할 수 있었습니다.

3. 문제 해결 예제 코드 📗

이제 위에서 알아보았던 Selector를 이용하여 이슈를 해결해보도록 하겠습니다! 코드는 제가 실제로 이슈를 해결하는 과정에 있던 코드를 이용하도록 하겠습니다.

아래에 보이는 코드는 이전에 무분별하게 API가 호출되는 로직의 코드입니다.

// hooks/useUserInfo 파일
import { useState, useCallback, useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { getUserPosts } from 'lib/api/user.api';
import { EResponse } from 'lib/enum/response';
import { EUserQuestion } from 'lib/enum/question';
import { userQuestionState } from 'lib/atom/question';
import { IQuestion } from 'types/question';
import usePageParam from 'hooks/util/usePageParam';
import usePagination from 'hooks/util/usePagination';

const useUserInfo = () => {
  const userIdx: number = usePageParam();
  const { setTotalPage } = usePagination();

  const [userPostTab, setUserPostTab] = useState<EUserQuestion>(EUserQuestion.WRITED);
  const [userQuestionList, setUserQuestionList] = useRecoilState<IQuestion[]>(userQuestionState);

  const requestUserPosts = useCallback(async (): Promise<void> => {
    try {
      const { status, data: { posts } } = await getUserPosts(userIdx, userPostTab);

      if (status === EResponse.OK) {
        setUserQuestionList(posts);
        setTotalPage(posts.length / CHUNK_POST_COUNT);
      }
    } catch (error) {
      console.log(error);
    }
  }, [setTotalPage, setUserQuestionList, userIdx, userPostTab]);

  useEffect(() => {
    requestUserPosts();
  }, [requestUserPosts]);
};

export default useUserInfo;

첫번째 단계로, selector를 관리하는 파일을 만들어서 아래의 코드를 작성해주었습니다.
코드를 읽기 전, selectorFamily라는 키워드가 보이실건데요, 크게 다를건 없습니다. selector에 비해서 selectorFamily는 함수를 호출할 때, 매개변수를 넘기는 함수일 때 사용하는 키워드 입니다.

// lib/selector/user.ts 파일
import { selectorFamily } from 'recoil';
import { getUserPosts } from 'lib/api/user.api';

type userQuestionSelectorParam = { // userQuestionSelector 매개변수 타입
  userIdx: number;
  userPostTab: EUserQuestion;
}

export const userQuestionSelector = selectorFamily<AxiosResponse, userQuestionSelectorParam>({
  key: 'userQuestionSelector',
  get: (param: userQuestionSelectorParam) => async () => {
    // 이미 요청했던 param.userIdx, param.userPostTab 값이 있다면 다시 API를 호출하지 않고 캐싱이 이루어집니다.
    const data = await getUserPosts(param.userIdx, param.userPostTab);
    return data;
  },
});

이제 이 selector를 사용하여 hooks를 수정해보겠습니다. 코드는 아래와 같습니다.

// hooks/useUserInfo.ts 파일
import { useState, useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import usePagination from 'hooks/util/usePagination';
import usePageParam from 'hooks/util/usePageParam';
import { isNullOrUndefined } from 'util/isNullOrUndefined';
import { CHUNK_POST_COUNT } from 'constants/util';
import { IQuestionListResponse } from 'types/question';
import { EUserQuestion } from 'lib/enum/question';
import { userQuestionState } from 'lib/atom/question';
import { userQuestionSelector } from 'lib/selector/question';

const useUserInfo = () => {
  const userIdx: number = usePageParam();
  const [userPostTab, setUserPostTab] = useState<EUserQuestion>(EUserQuestion.WRITED);
  const [userQuestionList, setUserQuestionList] = useRecoilState<IQuestion[]>(userQuestionState);
  
  const userQuestionResponse: IQuestionListResponse = useRecoilValue(
    userQuestionSelector({
      userIdx,
      userPostTab,
    }
  ));
  // state로 계속 변화되는 값을 selector에서 캐싱 처리해줍니다.
  // userQuestionResponse의 초기값은 selector에서 return된 값입니다.
  
  const { setTotalPage } = usePagination();
  
  const requestUserPosts = useCallback((): void => {
    if (!isNullOrUndefined(userQuestionResponse.data)) {
      const { posts } = userQuestionResponse.data;
      setUserQuestionList(posts);
      setTotalPage(posts.length / CHUNK_POST_COUNT);
    }
  }, [setTotalPage, setUserQuestionList, userQuestionResponse]);
}

export default useUserInfo;

이제 코드를 모두 바꿔주었으니, 실행을 해보도록 하겠습니다.

이전에 atom만을 사용하여 구현했을때는 개발자 도구에서 무분별하게 API가 호출되었지만, selector의 캐싱 기능을 이용하여 무분별한 API 호출을 줄이고, 데이터가 캐싱된 모습을 보실 수 있습니다. 😊

4. 글을 마치며 😶

이번에 selector를 도입하는 과정에서, 기존에 있던 로직과 이을 방법을 많이 고민했었는데, 여러가지 예제들을 따라해보거나 이렇게 해보면 되지 않을까? 하고 설계를 해나가면서 했던 것 같습니다.

selector로 교체하는 과정에서 놓친 부분들이 있다면 차츰차츰 고쳐 나갈 것 같습니다. 글 읽어주셔서 감사합니다!

profile
블로그 이전: https://yiyb-blog.vercel.app

3개의 댓글

comment-user-thumbnail
2021년 5월 17일

selector 쓰기도 쉽고 좋은것 같아서 도입해봐야겠어요!

1개의 답글
comment-user-thumbnail
2021년 6월 9일

저의 recoil 선생님....

답글 달기