React Recoil 비동기 처리하기
React Recoil 비동기 처리하기
React에서 Recoil을 이용하여 비동기 데이터를 처리하는 방법에 대해 알아보겠습니다.
1. Selector를 이용한 비동기 요청 사용하기
Recoil의 Selector를 이용하면 비동기 액션에 대한 처리가 가능합니다. 기존의 Redux에 비해 보다 간단하게 비동기를 처리할 수 있습니다.
예제 코드 작성을 위해 src 하위에 다음과 같이 파일 구조를 만들어줍니다.
src
├── services
│ ├── userService.ts
├── states
│ └── userState.ts
├── ...
App.tsx
index.tsx
비동기 요청을 담당할 serivces/userService.ts 파일을 다음과 같이 작성해줍니다.
/* services/userService.ts */
import {instance} from '../hooks/useAxiosLoader';
export const UserService = {
getUser: async (userId: number) => {
const {data} = await instance.get(
`/users/${userId}`,
{}
);
return data;
}
}
다음으로 state/userState.ts 파일을 다음과 같이 작성해줍니다. userSelector의 get 부분에 비동기 호출을 위해 async 함수를 작성해줍니다.
/* state/userState.ts */
import {atom, selector} from 'recoil';
import {UserService} from '../services/userService';
export const userSelector = selector({
key: 'userSelector',
get: async () => {
const response = await UserService.getUser(1);
return response;
}
});
마지막으로 App.tsx 파일을 작성해줍니다.
/* App.tsx */
import React from 'react';
import { userSelector } from './states/userState';
import { useRecoilValue } from 'recoil';
function App() {
const user = useRecoilValue(userSelector);
return (
<>
{JSON.stringify(user)}
</>
);
}
export default App;
여기까지 작성하고 애플리케이션을 실행해보면 정상적으로 동작할 것 같지만 다음과 같이 에러가 발생합니다.
Recoil에서는 비동기 호출시 발생하는 pending 상태에 대한 처리가 필요한데, React Suspense 또는 Loadable 객체를 이용한 방법이 있습니다.
1.1. Suspense
React Suspense를 이용하여 비동기 호출에 대한 pending 상태를 처리하려면 비동기 호출이 발생하는 컴포넌트에 대해 <React.Suspense>로 감싸주어야 합니다.
다음과 같이 App.tsx의 ReactDOM.render 부분을 수정해줍니다.
/* App.tsx */
ReactDOM.render(
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<App />
</React.Suspense>
</RecoilRoot>,
document.getElementById('root')
);
React Suspense에 대한 좀 더 자세한 내용은 아래 링크에서 확인할 수 있습니다.
1.2. Loadable
Loadable 객체는 atom 또는 selector의 상태를 나타냅니다. useRecoilValueLoadable, useRecoilStateLoadable Hook을 이용하여 사용할 수 있는데 selector의 set함수 포함 여부에 따라 용도 차이가 있습니다.
Loadable 객체는 다음과 같이 구성되어 있습니다.
- state
atom 또는 selector의 상태.
상태를 나타내는 3가지 값이 존재(hasValue, hasError, loading) - contents
각각의 상태에 따른 값.
hasValue(실제 값), hasError(Error 객체), loading(Promise 객체)
Loadable을 이용하여 비동기 호출의 pending 상태를 처리하는 경우엔 다음과 같이 수정해줍니다.
/* App.tsx */
import React from 'react';
import { userSelector } from './states/userState';
import { useRecoilValueLoadable } from 'recoil';
function App() {
const userLoadable = useRecoilValueLoadable(userSelector);
let user = '';
switch (userLoadable.state) {
case 'hasValue':
user = JSON.stringify(userLoadable.contents);
break;
case 'hasError':
user = userLoadable.contents.message;
break;
case 'loading':
user = 'Loading...';
break;
default:
user = 'Loading...';
};
return (
<>
{user}
</>
);
}
export default App;
다시 애플리케이션을 실행해보면 정상적으로 비동기 호출이 이루어지는 것을 확인할 수 있습니다.
2. 파라미터를 이용한 비동기 요청 사용하기
비동기 요청을 사용할 때 파라미터를 받아와서 사용하는 경우가 있습니다. 이러한 경우엔 selectorFamily를 사용해줍니다. selectorFamily를 사용하면 selector 객체에 파라미터를 전달하여 사용할 수 있습니다.
2.1. selectorFamily
state/userState.ts에서 앞서 구성했던 userSelector를 selectorFamily를 이용하여 수정해줍니다.
/* state/userState.ts */
export const userSelector = selectorFamily({
key: 'userSelector',
get: (userId: number) => async () => {
if (!userId) {
return;
}
const response = await UserService.getUser(userId);
return response;
}
});
다음으로 App.tsx 파일을 수정해줍니다.
/* App.tsx */
import React, {useState} from 'react';
import { userSelector } from './states/userState';
import { useRecoilValueLoadable } from 'recoil';
function App() {
const [userId, setUserId] = useState(0);
const userLoadable = useRecoilValueLoadable(userSelector(userId));
let user = '';
switch (userLoadable.state) {
case 'hasValue':
user = JSON.stringify(userLoadable.contents);
break;
case 'hasError':
user = userLoadable.contents.message;
break;
case 'loading':
user = 'Loading...';
break;
default:
user = 'Loading...';
};
const getUser = () => {
setUserId(userId + 1);
};
return (
<>
<button onClick={getUser}>[ GET USER ]</button>
<p/>
{user}
</>
);
}
export default App;
애플리케이션을 실행해보면 [ GET USER ] 버튼을 클릭할 때마다 userId가 업데이트되고 userSelector에 파라미터가 전달되어 호출되는 것을 확인할 수 있습니다.
2.2. selector의 캐싱
Recoil의 selector/selectorFamily는 모두 캐싱을 지원합니다. 따라서 비동기 쿼리를 구성한 후 새로고침을 하는 경우에 캐시키가 동일하다면 해당 selector/selectorFamily를 재실행하지 않고 캐시에 저장된 값을 반환합니다. 캐시키는 selector/selectorFamily 내부의 key, get함수, 전달된 파라미터를 기준으로 결정됩니다.
3. 동시성 모드로 비동기 요청 사용하기 (Concurrent Requests)
비동기 쿼리를 동시 요청으로 사용할 때는 가장 일반적으로 반복문을 이용하여 루프를 돌리는 방법을 생각해볼 수 있습니다. 하지만 리소스가 많이 소모된다면 Recoil에서 제공하는 waitForAll, waitForNone과 같은 동시성을 지원하는 함수를 사용하는 방법이 있습니다.
각각의 함수를 확인하기 위해 공통으로 사용할 App.tsx 파일을 다음과 같이 수정해줍니다.
/* App.tsx */
import React, {useState} from 'react';
import { usersSelector } from './states/userState';
import { useRecoilValueLoadable } from 'recoil';
function App() {
const usersLoadable = useRecoilValueLoadable(usersSelector);
let users = '';
switch (usersLoadable.state) {
case 'hasValue':
users = JSON.stringify(usersLoadable.contents);
break;
case 'hasError':
users = usersLoadable.contents.message;
break;
case 'loading':
users = 'Loading...';
break;
default:
users = 'Loading...';
};
return (
<>
{users}
</>
);
}
export default App;
3.1. waitForAll
waitForAll 함수는 selector 내부에서 다수의 요청이 동시에 발생했을 때 병렬로 요청을 처리해줍니다.
state/userState.ts에 다음과 같이 waitForAll을 사용하여 usersSelector를 추가해줍니다.
/* state/userState.ts */
export const usersSelector = selector({
key: 'usersSelector',
get: () => {
const userIdList: number[] = [1,2,3,4,5];
const users = waitForAll(
userIdList.map((userId: number) => userSelector(userId))
);
return users;
}
});
실행하여 확인해보면 요청의 response가 모두 정상적으로 완료되고나서 업데이트된 상태를 반환합니다.
3.2. waitForNone
waitForNone 함수도 waitForAll 함수와 마찬가지로 동시 요청에 대해 병렬 처리를 해주지만 결과값을 Loadable 객체로 받아 핸들링이 가능하다는 차이점이 있습니다.
state/userState.ts에서 앞서 추가한 usersSelector를 waitForNone을 사용하여 수정해줍니다.
/* state/userState.ts */
export const usersSelector = selector({
key: 'usersSelector',
get: ({ get }) => {
const userIdList: number[] = [1,2,3,4,5];
const userLoadables = get(waitForNone(
userIdList.map((userId: number) => userSelector(userId))
));
return userLoadables
.filter(({state}) => state === 'hasValue')
.map(({contents}) => contents);
}
});
실행하여 확인해보면 response가 정상적으로 완료된 요청부터 반환되는 것을 확인할 수 있습니다. 이렇게 사용하면 요청이 먼저 완료된 일부 데이터에 대해 추가적인 UI 업데이트를 적용할 수 있습니다.
이상으로 React에서 Recoil을 이용하여 비동기 데이터를 처리하는 방법에 대해 알아봤습니다.
※ Reference
- recoiljs.org, Asynchronous Data Queries, https://recoiljs.org/docs/guides/asynchronous-data-queries
- velog.io/@juno7803, [Recoil] Recoil 200% 활용하기, https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0
- blog.woolta.com, react의 새로운 상태관리 라이브러리 recoil 에 대해 알아보기 - atom, selector, https://blog.woolta.com/categories/1/posts/209
- taegon.kim, Recoil 레시피: 비동기 액션, https://taegon.kim/archives/10125