Recoil - 비동기 데이터 쿼리

Jin·2022년 8월 9일
1

recoil

목록 보기
2/2
post-thumbnail

https://recoiljs.org 의 내용을 타이핑 한것입니다.

동기 예제

user 이름을 얻기위한 간단한 동기 atom과 selector 예제

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

const currentUserNameState = selector({
  key :'CurrentserName',
  get : ({get}) => {
    return tableofUsers[get(currentUserIDState)].name;
  },
});


function CurrentUserInfo(){
  const userName = useRocilValue(currentUserNameState);
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RocoilRoot>
      <CurrentUserInfo/>
    </RecoilRoot>
  );
}

비동기 예제

만약 user의 이름을 쿼리해야하는데 DB에 저장되어있다면, Promise를 리턴하거나 async 함수를 사용하기만 하면 된다. 의존성에 하나라도 변경점이 생긴다면, selector는 새로운 쿼리를 재평가하고 다시 실행시킬 것이다. 그리고 결과는 쿼리가 유니크한 인풋이 있을 때에만 실행되도록 캐시된다.

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>
}

Selector의 인터페이스는 동일하므로 컴포넌트에서는 selector를 사용하면서 동기 atom 상태나 파생된 selector 상태, 혹은 비동기 쿼리를 지원하는지 신경 쓰지 않아도 괜찮다.

또한 React Suspense를 사용하여 보류중인 데이터(promise가 resolve 되기 전) 에 특정 UI를 렌더 시킬수 있다.

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

에러 처리하기

Recoil selector는 컴포넌트에서 특정 값을 사용하려고 할 때 어떤 에러가 생길지에 대한 에러를 던질수 있다. 이는 React <ErrorBoundary>로 잡을수 있다. 참고로 React ERrorBoundary는 현재 Class 형만 지원을 하고 있다. hook을 사용해 하고 싶다면 react-error-boundary 패키지를 설치하자.

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를 기반으로 쿼리를 하고 싶다고 가정해보자.

const userNameQuery = selectorFamily({
  key: 'UserName',
  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>
  );
}

Data-Flow Graph

쿼리를 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 reponse.error;
    }
    return resposne;
  },
});

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

function CurrentUserInfo() {
  const currentUser = useRecoilValue(currentUserInfoQuery);
  const friends = useRecoilValue(friendsInfoQuery);
  const setCurrentUserID = useSetReocilState(currentUserIDState);
  
  return (
    <div>
      <h1>{currentUser.name}</h1>
      <ul>{friends.map(friend => (<li key={friend.id} onClick={() => setCurrentUserID(friend.id)}> {friend.name} </li>))} </ul>
    </div>
  );
}

동시 요청

위의 예시에서, friendsInfoQuery는 쿼리를 이용해 각 친구에 대한 자료를 받아온다. 하지만 이를 루프하는것으로 기본적으로 직렬화 된다. 자원을 많이 사용한다면 waitForAll 과 같은 cocurrent helper를 사용하여 병렬로 돌릴수 있다.

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

waitForNone 을 사용해 일부 데이터로 추가적인 UI 업데이트를 할 수 있다.

const friendsInfoQuery = selector({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    const friendLoadables = get(waitForNone(friendList.map(friendID => userInfoQuery(friendID))),
    );
    return friendLodables.filter(({state}) => state ==='hasValue').map(([contents}) => contents);
  },
});

Pre-Fetching

성능 문제로 인해 렌더링 이전에 받아오기를 시작하고 싶을수도 있다. 그 방법은 렌더링을 하면서 쿼리를 진행할 수 있다. 위의 예시를 사용자가 유저계정을 바꾸기 위해 버튼을 누르자마자 다음 유저 정보를 받아오기 시작하는 형태로 만들어보자.

function CurrnetUserInfo() {
  const currentUse = userReocilValue(currentUserInfoQuery);
  const friends = useRecoilValue(friendsInfoQuery);
  
  const changeUser = userReocilCallback(({snapshot, set}) => (userID) => {
    snapshot.getLoadable(userInfoQuery(userID));
    // 유저 정보 미리 가져오기
    set(currentUserIDState, userID);
    // 새로운 렌더시작하기 위해 현재 유저 변경하기
  });
  
  return (
    <div>
      <h1>{currentUser.name}</h1>
      <ul>
        {friends.map((friend) => (
          <li key={friend.id} onClick={() => changeUser(friend.id)}>
            {friend.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

기본 Atom 값 쿼리

보통 Atom 을 사용해 변경 가능한 로컬 상태를 나타내지만 , promise를 사용하여 default value를 설정할수도 있다.

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

또한 selector를 사용하여 쿼리를 정의 하거나 다른 state에 의존할수도 있다. selecotr를 atom default value로설정한다면, 그 default는 동적이고 해당 selector가 업데이트 될때마다 업데이트 될것이다.

const UserInfoState = atom({
  key: 'UserInfo',
  default: selector({
    key: 'UserInfo/Default',
    get: ({get}) => myFetchUserInfo(get(currentUserIDState)),
  }),
});

또한 atomFamily로도 사용 될 수 있다.

const userInfoState = atomFamily({
  key: 'UserInfo',
  default: id => myFetchUserInfo(id),
});

const userInfoState = atomFamily({
  key: 'UserInfo',
  default: selectorFamily({
    key:'UserInfo/Default',
    get: id => ({get}) => myFetchUserInfo(id, get(paramState)),
  }),
});

React Suspense 를 사용하지 않는 비동기 쿼리

비동기 selecotr pending을 다루기 위해 React Suspense를 사용하는것은 필수가 아니다. useRecoilValueLoadable() 훅을 사용하여 렌더링 도중 현재 상태를 결정할 수 있다.

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

쿼리 새로고침

  • selector을 사용해 데이터쿼리를 모델링 할 때, selector 평가가 항상 주저인 상태에 대해 일관적인 값을 제공해야 한다.
  • Selector는 다른 atom과 selector 상태들에서 파생되는 상태들을 대표한다. 그러므로 selector 평가 함수들은 주어진 인풋에 관해 여러번 캐시되고 실행되더라도 idempotent(멱등 : 연산을 여러번 적용하더라도 결과가 달라지지 않는 성질) 해야 한다.
  • 하지만 selector가 데이터 쿼리로부터 얻은 값을 가지고 있는 상태라면, 새로운 데이터로 갱신이 필요할때 다시 쿼리하거나 쿼리에 실패한 이후 다시 시도할 수 있어야 한다.

쿼리를 갱신하거나 재시도하기 위해 다음과 같은 방법들을 사용할 수 있다.

1) useRecoilRefresher()

useReocilRefresher_UNSTABLE() 훅은 selector의 모든 캐시를 제거하고 강제로 다시 selector를 재평가할 수 있게 하는 콜백 함수를 제공한다.

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

function CurrentUserInfo() {
  const currentUserID = useRecoilValue(currentUserIDState);
  const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
  const refreshUserInfo = useRecoilRefresher_UNSTABLE(userInfoQuery(currentUserID));
  
  return (
    <div>
      <h1>{currentUserInfo.name}</h1>
      <button onClick={() => refreshUserInfo()}>Refresh</button>
    </div>
    );
}

2) Use a Request ID

Selector 평가는 인풋을 바탕으로 주어진 상태에 일관된 값을 제공해야한다. 따라서 요청 ID를 패밀리 매개변수 혹은 쿼리에 대한 종속성으로 추가할 수 있다.

const userInfoQueryRequestIDState = atomFamily({
  key: 'UserInfoQueryRequestID',
  default: 0,
});

const userInfoQuery = selectorFamily({
  key: 'UserInfoQuery',
  get: (userID) => async ({get}) => {
    get(userInfoQueryRequestIDState(userID));
    // request ID를 디펜던시로 추가
    const response = await myDBQuery({userID});
    if (response.error) {
      throw response.error;
    }
    return response;
  },
});

function useRefreshUserInfo(userId) {
  setUserInfoQueryRequestID = useSetReocilState(userInfoQueryRequestIDState(userID),);
  return () => {
    setUserInfoQueryReuqestId(requestID => requestID +1);
  };
}

function CurrentUserInfo() {
  const currentUserID = useRecoilValue(currentUserIDState);
  const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
  const refreshUserInfo = useRefreshUserInfo(currentUserID);

  return (
    <div>
      <h1>{currentUserInfo.name}</h1>
      <button onClick={refreshUserInfo}>Refresh</button>
    </div>
  );
}

3) Atom 사용하기

selector 대신 atom을 사용해 쿼리 결과를 모델링하는것. Atom 상태를 새로운 쿼리 결과를 독자적인 새로고침 방침에 맞춰 명령적으로 업데이트 할 수 있다.

const userInfoState = atomFamily({
  key:'UserInfo',
  default: (userID0 => fetch(userInfoURL(userID)),
});
  
function RefreshUserInfo({userID}) {
  const refreshUserInfo = useRecoilCallback(
      ({set}) => async (id) => {
        const userInfo = await myDBQuery({userID});
        set(userInfoState(userID), userInfo);
      },
      [userID],
    );
  
  useEffect(() => {
    const intervalID = setInterval(refreshUserInfo, 1000);
    return () => clearInterval(intervalID);
  }, [refreshUserInfo]);
  
  return null;
}
  • 이 방법에는 한가지 단점이 있다. Atom이 현재 원하는 동작일 경우, 쿼리 새로고치이 보류중인 동안 React suspense를 자동적으로 활용하기 위해 Promise를 새값으로 받아들이는 것을 지원하지 않는다.
  • 그러나 원한다면 로딩 상태와 결과를 수동으로 인코딩 하는 객체를 저장할 수 있다.

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

profile
내가 다시 볼려고 작성하는 블로그. 아직 열심히 공부중입니다 잘못된 내용이 있으면 댓글로 지적 부탁드립니다.

0개의 댓글