안녕하세요! 오늘은 TypeScript + React 환경에서 Recoil 전역 상태관리의 기능중 하나인 Selector를 이용하여 Axios의 데이터 값을 캐싱하는 방법에 대해서 알아보도록 하겠습니다.
아래는 이전에 Recoil 상태관리 라이브러리에 대해서 작성한 글입니다.
https://vo.la/ZeOOK
이번에 Selector에 대해서 제대로 알아보기 전, 저는 Recoil의 기능중 하나인 atom만을 사용하여 Custom Hook과 함께 전역 상태관리를 진행했습니다. 그저 기능은 순수하게 잘 작동하였지만 성능은 좋지 않았습니다. 그 이유는 바로 API가 호출된 값이 캐싱이 되지 않았었기 때문입니다.
포스트 탭을 전환할때마다 API가 무분별하게 호출됨.
거의 바뀌지 않는 데이터를 무분별하게 반복적으로 호출하게 된다면 좋지 않다고 생각이 들어서 개선할 방법을 생각하게 되었습니다. 🤔🤔
직접 API를 캐싱하여 성능을 개선하는 방법도 있었지만, atom과 연관된 Recoil 내에서의 방법이 있을 것 같아서 직접 찾아보았고, 평소에 큰 관심이 없었던 Selector의 기능을 알게되고난 후, 즉시 Selector를 도입해야겠다고 생각했습니다.
기본적으로, selector
를 이용하여 비동기 서버 통신시 얻을 수 있는 가장 큰 장점중, 자체적으로 캐싱을 지원하기 때문에 같은 입력값에 있어서 이전에 캐싱된 결과를 바로 보여주고, 퍼포먼스 면에서도 훨씬 유리한 장점이 존재합니다. 😁
즉, Selector의 캐싱을 이용하여 위의 이슈를 해결 할 수 있었습니다.
이제 위에서 알아보았던 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 호출을 줄이고, 데이터가 캐싱된 모습을 보실 수 있습니다. 😊
이번에 selector
를 도입하는 과정에서, 기존에 있던 로직과 이을 방법을 많이 고민했었는데, 여러가지 예제들을 따라해보거나 이렇게 해보면 되지 않을까?
하고 설계를 해나가면서 했던 것 같습니다.
selector
로 교체하는 과정에서 놓친 부분들이 있다면 차츰차츰 고쳐 나갈 것 같습니다. 글 읽어주셔서 감사합니다!
selector 쓰기도 쉽고 좋은것 같아서 도입해봐야겠어요!