React Query와 같은 server state를 관리해주는 기술이 널리 사용되고 있는 지금 굳이 global state library를 통해 server fetch data를 관리할 필요는 없겠지만 간혹 state의 초기 값을 server에서 fetch한 값으로 바로 사용해야 하는 경우가 존재한다. 이럴 경우 초기 값은 server에서 fetch한 값으로 사용하되 추후 해당 state 값을 user action을 통해 업데이트 되어야 하는 상황일 때 Jotai를 통해 어떻게 처리할 수 있을지 살펴보자
Derived State
이전 포스트에서 살펴본 바와 같이 Jotai는 Recoil과는 다르게 selector라는 별도의 개념이 없다, 즉 derived state 관리를 위해서 또 다른 atom을 선언하여 관리한다. 도입부에서 초기 값은 server에서 fetch한 값으로 사용하고 이후 user action을 통해 변경이 발생하면 변경한 값을 tracking해야 하는 상황이라면 다음과 같이 atom을 구성할 수 있을 것이다
type UserAtom = {
name: string;
age: number;
};
export const userAtom = atom<UserAtom | undefined>(undefined);
export const derivedAsyncUserAtom = atom(
async (get) => {
const derivedUserAtom = get(userAtom);
if (derivedUserAtom) {
return derivedUserAtom;
}
const result = await apiGetUserData(1);
return result;
},
(get, set, newVal: UserAtom) => {
set(userAtom, newVal);
}
);
제일 처음 get method를 통해 userAtom이 특정한 값을 가지고 있는지 파악하고 userAtom에 값이 있다면 해당 값을 반환하고 아무런 값을 가지고 있지 않다면 server에 user 데이터를 요청하여 해당 데이터를 반환한다
그리고 두 번째 param에서 set method를 통해 특정 user action을 통해 새로운 값이 전달되었을 때 해당 값을 userAtom에 선언하고 다음부터 derivedAsyncUserAtom이 새롭게 설정된 userAtom의 값을 사용하도록 업데이트 된다
derivedAsyncUserAtom을 사용할 때 주의해야 할 점은 derivedAsyncUserAtom은 asyn atom이기에 해당 atom을 사용하는 component는 suspense로 관련 처리를 해주어야 하며 atom에서 error가 throw되었을 때는 error boundary로 에러가 전달된다.
loadable
만약 위와 같은 동작을 원치 않을 경우 Jotai가 제공하는 loadable util을 통해 loading, success, error에 따른 처리를 별개로 관리할 수 있다 ( 즉, loading시 suspense를 trigger하지 않으며 atom에서 error가 throw되어도 바로 error boundary로 전달되지 않는다 )
다음의 예를 살펴보자
const loadableAtom = loadable(derivedAsyncUserAtom);
const Component = () => {
const [user] = useAtom(loadableAtom);
if (user.state === "loading") {
return <div>loading...</div>;
}
if (user.state === "hasError") {
console.log(user.error);
return <div>Error...</div>;
}
console.log(user.data)
...
loadable util을 통해 반환된 asynAtom을 사용하면 위의 예제와 같이 atom을 사용하는 component 내에서 state에 따라 원하는 조치를 해줄 수 있다
unwrap
loadable외의 unwrap util을 사용해 async atom을 suspense없이 처리할 수 있다. unwrap util은 두 번째 param으로 fallback 함수를 넘겨 받는데 현재 atom에 값이 설정되어 있지 않다면 fallback 함수에서 반환하는 값을 우선 사용하고 해당 atom의 값이 업데이트 되면 업데이트 된 값으로 교체해준다
const unwrapAtom = unwrap(derivedAsyncUserAtom, (prev) => {
return prev ?? "initial value";
});
const Component = () => {
const [user] = useAtom(unwrapAtom);
...
}
위의 예제에서 unwrap의 두 번째 param에 전달한 fallback 함수는 atom의 값이 있으면 해당 값을 반환하고 값이 없으면 "initial value"라는 string을 반환하고 있다.
만약 derivedAsyncUserAtom이 server로 부터 data를 fetch해 오는 시간이 3초 소요된다고 가정한다면 useAtom에서 반환하는 user 값의 3초 동안 "initial value"로 존재하다가 derivedAsyncUserAtom에서 data fetch 과정이 완료되고 새로운 값이 설정이 되면 user의 값은 새롭게 설정된 값으로 변경된다