Recoil async data queries 써보기

Min Su Kwon·2022년 10월 7일
1
post-thumbnail
post-custom-banner

현재 내가 재직중인 회사에서는 async data query 용도로는 react-query를, async 데이터가 포함되지 않는 전역 상태 관리에는 recoil을 사용하고 있다. react-query는 async data query에 굉장히 특화된 라이브러리로, 사용이 아주 쉽고 Stale-While-Revalidate 원칙에 맞게 잘 구현되어 있어서 사용자에게도 좋은 경험을 가져다준다.

그래서 (사용할 일이 없으니) recoil의 async data query 쪽에는 관심을 안가지고 있었는데, 꽤나 괜찮은 인터페이스를 제공하고 있는 것 같아서 한번 사용해봐야겠다는 생각이 들었다.

만들어 볼 것

sample_result
사용자는 사용자 리스트를 볼 수 있고, 사용자 하나를 클릭하면 가장 최근에 누른 사용자 정보를 상세하게 보여준다.

데이터 소스

간단한 샘플 유저 데이터를 반환해주는 json placeholder api를 활용한다

샘플 데이터

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

recoil selector 선언

recoil을 통해서 async 데이터를 받아오는 방법은 아주 간단하다. 기존에 다른 atom/selector에 의존하는 데이터를 만들 때 썼던 selector를 사용하되, get 콜백의 반환값으로 Promise를 주면 된다.

우선, 사용자 리스트를 받아오는 selector를 선언해보자. 편의를 위해 axios 라이브러리를 사용했다.

const userListAtom = selector<UserType[]>({
  key: "USER_LIST",
  get: async () => {
    const { data } = await axios.get<UserType[]>(
      "https://jsonplaceholder.typicode.com/users"
    );

    return data;
  }
});

async 함수의 반환값은 Promise이기 때문에, 이렇게만 해주면 선언이 끝난다.

다음은 사용자 한 명의 데이터를 받아오는 selector다. 이 selector는 userId를 받아서 해당 userId에 해당하는 사용자 데이터를 내려줘야 하기 때문에, selectorFamily로 선언해준다.

const userAtom = selectorFamily<UserType, number>({
  key: "USER",
  get: (userId) => async () => {
    const { data } = await axios.get<UserType>(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );

    return data;
  }
});

마찬가지로 간단하게 선언이 끝났다.

recoil selector로부터 데이터 받아쓰기

이제 이 selector를 활용해서 사용자 데이터를 실제로 받아온 뒤, 렌더링에 사용해보자. async selector의 경우에는 그냥 사용하면 안되고, Suspense + ErrorBoundary로 컴포넌트를 감싸주거나 (각각 loading, error 상태 핸들링을 위해서 사용된다) useRecoilValueLoadable 훅을 사용해서 state에 따른 분기처리를 해줘야한다.

Suspense + ErrorBoundary는 설명이 좀 복잡해질 것 같아서, 여기서는 Loadable을 사용해본다. useRecoilValueLoadable은 recoil에서 정의한 Loadable 인스턴스를 반환해주며, Loadable 인스턴스는 아래와 같은 프로퍼티들을 가지고 있다.

  • state: 현재 atom/selector의 상태. 아래 3개의 string 값 중 하나이다
    • hasValue
    • hasError
    • loading
  • contents: Lodable의 현재 값, state에 따라서 값이 다르다.
    • hasValue : Promise에서 반환한 실제 데이터 값
    • hasError : 던져진 에러 객체
    • loading : Promise

훅을 사용한 뒤, 반환된 state값에 맞춰서 상황에 알맞은 것을 렌더링해주면 된다.

const UserList = (props: {
  onSelectUser: (userId: number) => void;
}): JSX.Element => {
  const { onSelectUser } = props;
  const { state, contents } = useRecoilValueLoadable(userListAtom);

  if (state === "hasError") {
    return <div>Error : {contents.message}</div>;
  }

  if (state === "loading") {
    return <div>Loading...</div>;
  }

  return (
    <ul>
      {_.map(contents, (user) => {
        return (
          <li
            key={user.id}
            onClick={() => onSelectUser(user.id)}
            style={{ cursor: "pointer" }}
          >
            {user.id} : {user.name}
          </li>
        );
      })}
    </ul>
  );
};

선택된 사용자 카드도 비슷한 방법으로 렌더링해준다.

const SelectedUserCard = (props: { userId: number }): JSX.Element | null => {
  const { userId } = props;

  const { state, contents: user } = useRecoilValueLoadable(userAtom(userId));

  if (!userId) {
    return <div>Please select user </div>;
  }

  if (state === "loading") {
    return <div>Loading...</div>;
  }

  if (state === "hasError") {
    return null;
  }

  return (
    <>
      <div>
        Selected User = {user.name}
        <ul>
          <li>{user.username}</li>
          <li>{user.email}</li>
          <li>
            <span role="img" aria-label="phone">
              📞
            </span>{" "}
            {user.phone}
          </li>
        </ul>
      </div>
    </>
  );
};

그리고 두 컴포넌트를 사용하는 메인 앱. useState로 상태값을 정의했는데, 이 부분도 recoil로 정의해도 무방하다.

export default function App(): JSX.Element {
  const [selectedUserId, setSelectedUserId] = useState(0);

  return (
    <RecoilRoot>
      <UserList onSelectUser={setSelectedUserId} />
      <SelectedUserCard userId={selectedUserId} />
    </RecoilRoot>
  );
}

느낀점 & 궁금한점

  • 렌더링 로직이 에러/로딩/정상 상태로 잘 구분되어 있어서 보기 편하고, 어떤 상황에 어떤 것이 렌더링되는지 명확하다
  • 이 친구들을 사용할 때 async atom/selector인지 아닌지 잘 확인하고 사용해야 할 것 같다. 로딩상태나 에러상태를 잡아주는 친구들이 없으면 app crash가 날 것이다. atom/selector 이름에 async prefix나 postfix를 붙여주면 잘 구분할 수 있지 않을까 생각이 듬
  • get은 알겠는데, 데이터 바꿀려면(set) 어떻게 해야하는거지?
  • refresh 하고싶은 경우 방법이 있을까?

Codesandbox

Reference

profile
이제 막 커리어를 시작한 소프트웨어 엔지니어입니다. 배운 것을 정리하면서 조금 더 깊이 이해하려는 습관을 들이려고 합니다. 피드백은 언제나 환영입니다.
post-custom-banner

0개의 댓글