[Recoil] Selector + useRecoilValueLoadable을 활용해서 API값 가져오기 🎎

yiyb0603·2021년 7월 10일
9

React

목록 보기
15/15
post-thumbnail

안녕하세요! 오늘은 TypeScript + React 환경에서 전역 상태관리 라이브러리 Recoil을 활용한 Selector + useRecoilValueLoadable을 활용해서 API값 가져오기 라는 주제로 글을 작성해보도록 하겠습니다.

이전에 Recoil Selector와 관련된 글인 API 값 캐싱하기를 작성한 경험이 있었는데요, 이를 활용해서 각종 개발을 하다가 애매모호한 상황을 마주한적이 있는데, 이를 useRecoilValueLoadable을 활용해서 해결했던 경험을 바탕으로 글을 작성해보려고 합니다 :)

1. 이전의 Suspense fallback 로딩쪽에서 겪었던 문제 🎃

const ProfilePage = React.lazy(() => import('./ProfilePage')); // 지연 로딩

// 프로필을 불러오는 동안 스피너를 표시합니다
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>

React.js에서 데이터를 가져오기 위한 Suspense는 를 사용하여 선언적으로 데이터를 비롯한 무엇이든 기다릴수 있도록 해주는 새로운 기능이며, fallback props에 로딩을 표시할 컴포넌트를 넣어주면 앞서 설명했던 Recoil Selector를 이용해서 API 값을 가져오는 동안 보여줄 로딩을 추가할 수 있습니다.
(Recoil Selector 뿐만이 아닌, 예시 코드의 컴포넌트 지연로딩 등등 여러곳에서 지원합니다.)

저는 API를 캐싱해주는 기능을 가진 Selector를 이용해서 API를 자주 가져오고 손쉽게 활용하고 있었는데요, 하지만 경우에 따라서 보여주지 말아야할 Suspense가 selector를 사용할 때마다 불필요하게 보여지고 했었습니다.

const userQuestionResponse: IQuestionListResponse = useRecoilValue(
  userQuestionSelector({
    userIdx,
    userPostTab,
  }
));

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]);

위는 제가 이전까지 작성해오던 방식의 selector를 이용한 API 값을 가져오는 로직인데요, 위처럼 적게되면 Suspense 로딩을 표시하지 않으려고 해도 표시가 되는 바람에 많은 고민을 했었습니다. 😟

2. useRecoilValueLoadable이라는 것을 써볼까? 🧪

저는 위의 코드와 유사한 useRecoilValue를 보고나서 공식문서에 useRecoilValueLoadable 이라는 리코일 Hooks도 있었는데, 이 두가지의 차이점이 무엇일까? 하고 리코일 공식문서를 다시 찾아보게 되었습니다.

useRecoilValueLoadable의 공식문서를 보는중, 아래의 코드를 보게 되었습니다.

function UserInfo({userID}) {
  const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
  switch (userNameLoadable.state) {
    case 'hasValue':
      return <div>{userNameLoadable.contents}</div>;
    case 'loading':
      return <div>Loading...</div>;
    case 'hasError':
      throw userNameLoadable.contents;
  }
}
  

해당 코드를 보고나서, useRecoilValueLoadable을 쓰게된다면 데이터 로딩 타이밍에 로딩화면을 개발자가 원하는 화면으로 넣어볼 수 있지 않을까? 라고 생각을 하게 되었고, 이전에 작성했던 방식에서 useRecoilValueuseRecoilValueLodable로 변경해보면서 그에 따른 로직도 변경해보았습니다.

아래는 jsonplaceholder 라는 더미 데이터 API를 사용해서 로직을 구현해보았습니다.

아래는 selector를 선언한 코드입니다.

import { selector } from 'recoil';
import { fetchTodos } from 'lib/api/todo.api';

export const fetchTodosSelector = selector({
  key: 'fetchTodosSelector',
  get: async () => {
    const data = await fetchTodos(); // jsonplaceholder API 호출 함수
    return data;
  },
});  

아래는 selector를 가져와서 구현한 hooks의 코드입니다.

import { useCallback, useEffect, useState } from 'react';
import { useRecoilState, useRecoilValueLoadable } from 'recoil';
import { todoListState } from 'lib/recoil/todo.atom';
import { ITodo } from 'types/todo.type';
import { fetchTodosSelector } from 'lib/recoil/todo.selector';

const useFetchTodos = () => {
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const [isError, setIsError] = useState<boolean>(false);
  const [todos, setTodos] = useRecoilState<ITodo[]>(todoListState);

  const todosResponse = useRecoilValueLoadable(fetchTodosSelector);

  const requestFetchTodos = useCallback((): void => {
    if (todosResponse === null || todosResponse === undefined) {
      return;
    }

    switch (todosResponse.state) {
      case 'loading':
        setIsLoading(true);
        break;

      case 'hasValue':
        setIsLoading(false);
        setTodos(todosResponse.contents);
        break;

      case 'hasError':
        setIsError(false);
        setIsLoading(false);
        break;

      default:
        return;
    }
  }, [setTodos, todosResponse]);

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

  return {
    isLoading,
    isError,
    todos,
  };
}

export default useFetchTodos;  

selector의 정보를 useRecoilValueLoadable에 담게되면 state라는 객체를 통해서 해당 요청이 로딩중인지, 에러가 발생했는지, 값을 올바르게 받았는지에 대한 상태를 알 수 있습니다.

그리고 해당 hooks에서 로딩, 에러, 리스트의 정보를 return 해주었기 때문에, 이 값들을 통해서 렌더링의 조건을 정의해줄수 있었습니다.

참고로 로딩 상태에서 원하는 렌더링 조건을 정해주었기에, Suspense의 로딩이 뜨지 않아서 좋았습니다.

import useFetchTodos from 'hooks/useFetchTodos';

const Todos = (): JSX.Element => {
  const { isLoading, isError, todos } = useFetchTodos();

  if (isLoading) {
    return <div>로딩중입니다.</div>
  }

  if (isError) {
    return <div>오류가 발생했어요!</div>
  }

  return (
    <div>
      {
        todos.map((todo) => (
          <div key={todo.id}>{todo.title}</div>
        ))
      }
    </div>
  );
}

export default Todos;  

3. 글을 마치며

오늘은 간단하게 useRecoilValueLoadable을 이용해서 Suspense에서의 불편했던 점을 해결했던 경험에 대해서 글을 작성해보았습니다. 다음에도 유익한 정보로 찾아뵙겠습니다! 글 읽어주셔서 감사합니다 :)

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

3개의 댓글

comment-user-thumbnail
2021년 8월 24일

되게 우아한 비동기 처리내요!
저도 한번 적용해 보겠습니다.!

답글 달기
comment-user-thumbnail
2021년 9월 27일

case 'hasError':
setIsError(false);
setIsLoading(false);

이부분에서 setIsError(true) 가 맞을것같군요~ 좋은글 잘봤습니다.

답글 달기
comment-user-thumbnail
2022년 6월 8일

포스팅 잘 읽었습니다. 리액트 쿼리의 useQuery와 유사하게 작동하는 것 같네요. 이번에 업데이트된 서스펜스까지 합치면 프론트엔드 단계에서 구현할 수 있는 방향이 정말 무궁무진해진 것 같아요

답글 달기