Recoil 비동기 사용하기

프론트엔드 퍼즐러·2023년 11월 20일

Recoil

목록 보기
2/2

지난 글에 이어서 Recoil에서 비동기로 서버와 통신하고 상태관리를 어떻게 할 수 있는지 공식 문서를 보고 정리하고 강아지 사진 api를 이용해서 간단한 실습도 해보겠습니다.

빠른 시작

recoil 공식 문서에 나와있는 간단한 예제는 유저이름을 db로부터 가져오는 예제 입니다. 아주 간단하게 asyncawait으로 유저이름을 불러올 수 있습니다.

const currentUserIDState = atom({
  key: 'CurrentUserID',
  default: 1,
});

const currentUserNameQuery = selector({
  key: 'CurrentUserName',
  get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    return response.name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameQuery);
  return <div>{userName}</div>;
}

로딩화면 보여주기

recoilpromiseresolve 되기 전에 컴포넌트를 Suspense의 경계로 감싸는 것으로 아직 보류중인 하위 항목들을 잡아내고 대체하기 위한 UI를 렌더합니다. 즉 db로부터 데이터를 받기 전 로딩중인 화면을 보여줄 수 있습니다.

function MyApp() {
  return (
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
        <CurrentUserInfo />
      </React.Suspense>
    </RecoilRoot>
  );
}

에러 처리하기

만약 비동기 통신중에 에러가 있다면 Recoil selector는 컴포넌트에서 특정 값을 사용하려고 할 때에 어떤 에러가 생길지에 대한 에러를 던질 수 있습니다. 이는 <ErrorBoundary>로 잡을 수 있습니다.

const currentUserNameQuery = selector({
  key: 'CurrentUserName',
  get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    if (response.error) {
      throw response.error;
    }
    return response.name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameQuery);
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <CurrentUserInfo />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}

매개변수 쿼리

매개변수를 기반으로 쿼리를 하고싶을 때가 있을 수 있습니다. 예를 들어 컴포넌트 props를 기반으로 쿼리를 하고 싶다고 해봅시다. 이 때 selectorFamily helper를 사용할 수 있습니다.

여기서 selectorFamily란 selector와 유사한 강력한 패턴입니다. 다만, get, set, selector의 콜백을 매개변수로 전달할 수 있다는 점이 다릅니다.

const userNameQuery = selectorFamily({
  key: 'UserName',
  // 콜백으로 전달 받은 아이디를 비동기로 db와 통신
  get: (userID) => async () => {
    const response = await myDBQuery({userID});
    if (response.error) {
      throw response.error;
    }
    return response.name;
  },
});

function UserInfo({userID}) {
  const userName = useRecoilValue(userNameQuery(userID));
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <UserInfo userID={1} />
          <UserInfo userID={2} />
          <UserInfo userID={3} />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}

자동 데이터 쿼리

쿼리를 selector로 모델링하면 상태와 파생된 상태, 그리고 쿼리를 혼합한 데이터 플로우 그래프를 만들 수 있습니다! 이 그래프는 상태가 업데이트 되면 리액트 컴포넌트를 업데이트하고 리렌더링합니다.

다음 예시는 최근 유저의 이름과 그들의 친구 리스트를 렌더합니다. 만약 친구의 이름이 클릭되면, 그 이름이 최근 유저가 되며 이름과 리스트는 자동적으로 업데이트 됩니다.

const currentUserIDState = atom({
  key: 'CurrentUserID',
  default: null,
});

const userInfoQuery = selectorFamily({
  key: 'UserInfoQuery',
  get: (userID) => async () => {
    const response = await myDBQuery({userID});
    if (response.error) {
      throw response.error;
    }
    return response;
  },
});

const currentUserInfoQuery = selector({
  key: 'CurrentUserInfoQuery',
  get: ({get}) => 
  // UserId로 UserInfo를 불러온다.
  get(userInfoQuery(get(currentUserIDState))),
});

const friendsInfoQuery = selector({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    return friendList.map((friendID) => get(userInfoQuery(friendID)));
  },
});

function CurrentUserInfo() {
  const currentUser = useRecoilValue(currentUserInfoQuery);
  const friends = useRecoilValue(friendsInfoQuery);
  
  // 이름을 클릭하면 최근 유저가 바뀌고 최근 유저의 친구 리스트가 업데이트 된다.
  const setCurrentUserID = useSetRecoilState(currentUserIDState);
  return (
    <div>
      <h1>{currentUser.name}</h1>
      <ul>
        {friends.map((friend) => (
          <li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
            {friend.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <CurrentUserInfo />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}

실습

위의 내용을 바탕으로 이제 버튼을 누르면 랜덤 강아지 사진을 불러오는 어플을 만들어 보겠습니다.

axios 설치

원활하게 데이터를 가져오기 위해 axios를 설치합니다.

npm i axios

yarn add axios

RecoilRoot

마찬가지로 처음에 제일 상위 루트 컴포넌트를 RecoilRoot으로 감싸줍니다.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import { RecoilRoot } from "recoil";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
);

전역 상태 만들기 (Atom, Selector 세팅)

전역으로 상태 관리를 하기 위해서 dog-state.tsatomselector를 세팅해 줍니다.

강아지 종에 따라 다른 랜덤 이미지를 가져오게 하기 위해서 atom은 강아지 종 상태를 지정하고 selector는 강아지 이름에 따라 다른 이미지url을 반환하게 합니다.

import axios from "axios";
import { atom, selector } from "recoil";

export const dogNameState = atom({
  key: "dogName",
  default: "hound",
});

export const getDogImages = selector({
  key: "get/github-repo-stars",
  get: async ({ get }) => {
    const name = get(dogNameState); // atom 정보를 얻어옴
    if (!name) return;
    const url = `https://dog.ceo/api/breed/${name}/images/random`;
    try {
      const response = await axios(url); // 얻어온 atom 정보를 활용하여 url fetch
      const data = await response.data.message;
      return data;
    } catch (err) {
      throw Error("잘못된 강아지 정보입니다!");
    }
  },
});

Atom, Selector 사용하기

이제 app.tsxselect로 이미지 종류를 선택하게하고 사용자가 다른 종을 선택하면 이미지를 변경시켜 보도록 하겠습니다. 또한 Suspense로 이미지를 불러오기 전 로딩중 이라는 메세지도 포함하였습니다.

import { useRecoilState } from "recoil";
import "./App.css";
import { Suspense } from "react";
import { dogNameState } from "./dog-state";
import DogImage from "./dog-image";

function App() {
  const [dogName, setDogName] = useRecoilState(dogNameState);

  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setDogName(e.target.value);
  };

  return (
    <Suspense fallback={<div>로딩중...</div>}>
      <div className="App">
        <label>
          강아지 종류 선택:
          <select value={dogName} onChange={handleChange}>
            <option value={"hound"}>Hound</option>
            <option value={"akita"}>Akita</option>
            <option value={"pug"}>Pug</option>
          </select>
        </label>
        <div>
          <DogImage />
        </div>
      </div>
    </Suspense>
  );
}

export default App;

DogImage.tsx

// DogImage.tsx

import { useRecoilValue } from "recoil";
import { getDogImages } from "./dog-state";

function DogImage() {
  const imageUrl = useRecoilValue(getDogImages);
  return <img src={imageUrl} alt="랜덤 강아지" style={{ maxWidth: "100%" }} />;
}

export default DogImage;

실습 확인

codesandbox로 코드를 보면서 확인해 보세요

참고 사이트

https://recoiljs.org/ko/docs/guides/asynchronous-data-queries

profile
기초를 다지고 있는 개발자

0개의 댓글