지난 글에 이어서 Recoil에서 비동기로 서버와 통신하고 상태관리를 어떻게 할 수 있는지 공식 문서를 보고 정리하고 강아지 사진 api를 이용해서 간단한 실습도 해보겠습니다.
recoil 공식 문서에 나와있는 간단한 예제는 유저이름을 db로부터 가져오는 예제 입니다. 아주 간단하게 async과 await으로 유저이름을 불러올 수 있습니다.
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>;
}
recoil은 promise가 resolve 되기 전에 컴포넌트를 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를 설치합니다.
npm i axios
yarn add axios
마찬가지로 처음에 제일 상위 루트 컴포넌트를 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>,
);
전역으로 상태 관리를 하기 위해서 dog-state.ts에 atom과 selector를 세팅해 줍니다.
강아지 종에 따라 다른 랜덤 이미지를 가져오게 하기 위해서 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("잘못된 강아지 정보입니다!");
}
},
});
이제 app.tsx에 select로 이미지 종류를 선택하게하고 사용자가 다른 종을 선택하면 이미지를 변경시켜 보도록 하겠습니다. 또한 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
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