React) Recoil selector로 비동기 작업 수행하기

2ast·2022년 10월 6일
2
post-custom-banner

[Recoil selector 만져보기]에 이은 2탄: 비동기 selector 만들기

recoil의 selector는 그 자체적으로 비동기를 지원한다. 그래서 이참에 이전에 만들어본 환전 시스템을 실제 api를 호출하여 환율 정보를 가져와 반영하도록 예제를 만들어볼 예정이다.

비동기로 데이터를 가져오는 방법

selector에서 비동기를 사용하는 방법은 간단하다. 그냥 get 함수를 async로 선언하면 끝이다. 아래는 이번에 사용할 예제코드 중 일부인데, 환율 정보 api를 사용해 원달러 환율 정보를 가져오고, 그 값을 기준으로 원화를 달러로 바꿔 리턴하는 selector를 구현한 것이다.

export const realTimeDollarState = selector({
  key: "realTimeDollarState",
  get: async ({ get }) => {
    const won = get(wonState);
    const res = await fetch(
      "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD"
    );
    const result = await res.json();
    const exchangeRate = result[0].basePrice;

    return won / exchangeRate;
  },
});

다만 이렇게 했다고 바로 프로젝트에 selector를 사용할 수 있는 것은 아니고, 비동기 데이터가 아직 오지 않았을 때 화면을 어떻게 처리할 것인지를 판단하는 로직을 추가해야한다.(이건 선택사항이 아니라 필수사항이다.) 이때 많이 사용되는 방법이 바로 Suspense이다.

import Won from "./components/Won";
import Dollar from "./components/Dollar";
import React from "react";

const App = () => {
  return (
    <div>
      <Won />
      <React.Suspense fallback={<div>Loading...</div>}>
        <Dollar />
      </React.Suspense>
    </div>
  );
};
export default App;

이처럼 비동기 selector를 호출하는 Dollar 컴포넌트를 Suspense로 감싸서 데이터가 loading중일 때는 Loading...이라는 문구가 노출되도록 설정해주었다. 이렇게 하면 비동기 get 함수의 준비는 끝났다. 이제 똑같이 달러를 입력받았을 때 api를 호출하여 원화로 바꿔주는 비동기 setter만 설정해주면 마무리 된다.

라고 생각하던 때가 있었다...

Selector는 비동기 set을 지원하지 않는다...


호기롭게 set 함수를 비동기로 선언하고 api를 호출하는 순간 이 에러와 마주쳤다. 그렇다. getter와는 다르게 setter는 비동기를 지원하지 않는 것이다. 그러면 만약에 지금과 같이 비동기로 데이터를 가져와 그 값을 기준으로 값을 셋팅해야할 때는 어떻게 해야할까?

내가 구글링을 해서 찾아낸 가장 괜찮은 해결책은 custom hook을 이용하는 것이다. selector에서 직접 비동기 작업을 할 수 없다면 중간에 custom hook을 배치하여 hook 내부에서 비동기 작업을 수행하고 그 값을 set 함수에 넘기는 방법이다.

//stroe.js
export const realTimeDollarState = selector({
  key: "realTimeDollarState",
  get: async ({ get }) => {
    const won = get(wonState);
    const res = await fetch(
      "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD"
    );
    const result = await res.json();
    const exchangeRate = result[0].basePrice;

    return won / exchangeRate;
  },
  set: ({ set }, exChangedWon) => {
    set(wonState, exChangedWon);
  },
});


//useRealTimeDollar.js
import { useEffect, useState } from "react";
import { useRecoilStateLoadable } from "recoil";
import { realTimeDollarState } from "./atom/store";

const useRealTimeDollar = () => {
  const [dollar, setRecoilState] = useState(0);
  const [loadable, setRealTimeDollar] =
    useRecoilStateLoadable(realTimeDollarState);

  useEffect(() => {
    if (loadable.state === "hasValue") {
      setRecoilState(loadable.contents);
    } else if (loadable.state === "hasError") {
      console.log(loadable.contents);
    }
  }, [loadable]);

  const setDollar = async (inputDollar) => {
    const res = await fetch(
      "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD"
    );
    const result = await res.json();
    const exchangeRate = result[0].basePrice;
    const exchangedWon = exchangeRate * inputDollar;
    setRealTimeDollar(exchangedWon);
  };

  return { dollar, setDollar };
};

export default useRealTimeDollar;

여기서 등장하는 useRecoilStateLoadable에 대해서는 [useRecoilValueLoadable을 사용해보자] 글을 읽어보면 도움이 될 것 같다. 쉽게 말해서 비동기 state를 가져올 때 해당 state의 로딩상태를 함께 제공해주는 함수이다. custom hook 내부에서 로직을 처리하기 위해 Suspense를 적용하기에는 부적절하므로, 대안으로 찾은 방법이 바로 useRecoilStateLoadable이었다.

useRealTimeDollar hook의 내부 구조는 간단하다. 먼저 dollar를 useState로 선언하고 기본값을 0으로 설정한다. 그리고 dollar를 return하는 것이다. 여기서 useEffect를 통해 loadable의 상태가 hasValue로 바뀌면 받아온 값을 dollar에 할당하는 구조를 갖고 있다.

setter의 경우 selector에서 직접적으로 비동기 작업을 실행할 수 없으니 setDollar라는 새로운 함수를 선언하고 그 안에서 비동기로 데이터를 가져와 setRealTimeDollar 함수를 호출하는 형태다.

이제 실행을 해보자

import React, { useEffect, useState } from "react";
import useRealTimeDollar from "../useRealTimeDollar";

const Dollar = () => {
  const { dollar, setDollar } = useRealTimeDollar();

  const [text, setText] = useState("");

  const onChange = (e) => {
    setText(e.target.value);
  };

  useEffect(() => {
    setText(dollar);
  }, [dollar]);

  return (
    <div>
      달러를 입력해주세요.
      <input value={text} onChange={onChange} />
      <button
        onClick={() => {
          setDollar(Number(text));
        }}
      >
        환전하기
      </button>
    </div>
  );
};

export default Dollar;

이렇게 프로젝트에 hook을 연결해주고 실행을 하면

잘 작동한다 (Good...)

참고 링크:https://github.com/facebookexperimental/Recoil/issues/762
참고 링크:https://velog.io/@yiyb0603/Recoil-Selector-useRecoilValueLoadable%EC%9D%84-%ED%99%9C%EC%9A%A9

profile
React-Native 개발블로그
post-custom-banner

0개의 댓글