이번 프로젝트에 react-query를 서버 상태관리 라이브러리로 사용하게 되면서 전역상태관리를 위해 당연스레 사용했었던 redux와 redux-toolkit을 사용하는게 다소 부담스럽다고 생각이 되었기 때문에 비교적 러닝커브가 낮고 보일러플레이트가 적은 recoil을 선택해 써보게되었다.
리코일에서 소개되어있는 기본적인 데이터 플로우 그래프는 다음과 같다.
[atom(공유 상태)] → [Selector(순수 함수)] → [컴포넌트]
우리 프로젝트에서 전역으로 관리되고 모든 컴포넌트에서 구독되어야하는 데이터는 현재 로그인된 유저의 데이터이다. 초기에는 로그인 후 발급되는 session의 유무를 활용해서 recoil의 atom상태에 저장하고 구독하고자 했다.
useSetRecoilState
로 atom에 저장useRecoilValue
로 상태 구독이렇게했던 가장 큰 이유는 컴포넌트에서 유저 데이터가 필요할 때마다 재요청을 하지 않고 현재 로그인한 유저가 변하지 않는다면 최상단에서 한번의 요청으로 상태를 유지하기 위함이었다.
상세페이지에서 댓글 작성이나 조회 수, 찜 업데이트를 할 때 현재 로그인된 유저의 id가 필요하고 필요할 때마다 요청을 보내지 않기 위해 리코일로 데이터를 관리하려고 한 것인데 페이지 새로고침을하면 값이 유지되지 못했다. 필요할 때마다 새로운 요청을 보내지 않으면서 리코일 상태가 유지되는 방법이 필요했다.
Atom Effects는 부수효과를 관리하고 Recoil의 atom을 초기화 또는 동기화하기 위해 리코일에서 제공하는 API이다.
atom의 옵션에 localStorageEffect를 추가하고 로컬스토리지key를 'current_user'로 설정해 주었다.
const localStorageEffect: <T>(key: string) => AtomEffect<T> =
(key: string) =>
({ setSelf, onSet, trigger }) => {
const loadPersisted = () => {
const savedValue =
typeof window !== "undefined" ? localStorage.getItem(key) : undefined;
if (!savedValue) return;
setSelf(JSON.parse(savedValue as string));
};
if (trigger === "get") {
loadPersisted();
}
// onSet -> Subscribe to changes in the atom value.
onSet((newValue, oldValue, isReset) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
const userEmailState = atom({
key: `userEmail/${v1()}`,
default: "",
effects_UNSTABLE: [localStorageEffect("current_user")],
});
이제 useSetRecoilState등으로 set이 감지되고 새로운 값이 들어온것이 확인되면 로컬스토리지에 저장되고 필요할 때 useRecoilValue등으로 get이 감지된다면 loadPersisted함수가 실행되고 저장되어있는 값을 읽을 수 있게 된다.
위에서 해결한 내용에는 한 가지 문제가 남아있었다.새로고침시 값을 유지할 수 있게 되었지만 나의 경우 모든 유저의 데이터를 가져오고 있었기 때문에 로컬스토리지에 모든 데이터를 저장하는 부분이 마음에걸렸고 최소한의 데이터만 저장하게 하기위해 다시 한번 수정이 필요했다.
여기서 공식문서의 비동기 데이터 쿼리부분을 정독했고 selector가 단순히 값 자체를 리턴하는 것 이상으로 비동기 데이터를 리코일의 데이터 플로우 그래프에 포함시킬 수 있고 Promise를 반환할 수 있다는 것을 알 수 있었다.
const myQuery = selector({
key: 'MyQuery',
get: async ({get}) => {
return await myAsyncQuery(get(queryParamState));
}
});
- selector는 비동기 함수를 가지고 있을 수 있고 Promise를 반환해줄 수 있다.
- selector는 상태에서 파생된 데이터로 다른 atom에 의존하는 동적인 데이터를 만들 수 있게한다.
- 의존하고 있는 값이 동일한 경우에는 캐시에 있는 값을 반환하고 의존값이 변경된 경우만 새로운 요청을 한다.
공식문서의 data-flow-graph부분을 참고해서 유저 데이터의 흐름을 비동기로 가져올 수 있도록 코드를 수정했다.
const userEmailState = atom({
key: `userEmail/${v1()}`,
default: "",
effects_UNSTABLE: [localStorageEffect("current_user")],
});
const userInfoQuery = selectorFamily({
key: `UserInfoQuery/${v1()}`,
get: userEmail => async () => {
const email = userEmail as string;
const response = await apiGet.GET_USER(email);
if (response.error) {
throw response.error;
}
return response.user;
},
cachePolicy_UNSTABLE: {
eviction: "most-recent",
},
});
const currentUserInfoQuery = selector({
key: `CurrentUserInfoQuery/${v1()}`,
get: ({ get }) => {
const currentUserEmail = get(userEmailState);
if (currentUserEmail === "") return undefined;
const userInfo = get(userInfoQuery(currentUserEmail));
return userInfo;
},
});
이제 로컬스토리지에는 유저의 이메일만 저장되고 저장된 이메일을 selectorFamily에서 매개변수로 활용해 전체 유저의 데이터를 가져오고 이를 컴포넌트에서 구독할 수 있게 되었다. userEmail의 값이 변경된다면 자동으로 새로운 유저데이터를 가져올 것이다.
cachePolicy_UNSTABLE
selector와 selectorFamily는 기본적으로 캐싱을 지원한다. 이 점이 장점이기도 하지만 메모리 누수 문제가 있다는 단점도 존재한다.. (매개변수의 값이 변경된다면 값이 새로 쓰이고 기존 값은 사용되지 않더라도 지워지지 않는다. 즉, 값이 캐싱된 상태로 메모리에 남아있다. → 메모리 누수가 발생한다.)가장 문제가 심각한것이 selectorFamily인데 그때문인지 selectorFamily에는 cachePolicy 옵션을 추가할 수 있다. 기본값은 keep-all이기 때문에 항상 유지되고 lru나 most-recent를 사용하면 메모리를 조금은 관리할 수 있다.
관련내용 참고 블로그
이전에는 상태값을 읽어오기 위해 useRecoilValue를 사용했다. getRecoilValueLoadable은 useRecoilValue과 비슷하지만 비동기 값을 반환하는 selector값을 읽어오기 위해 만들어졌다.
getRecoilValueLoadable은 state와 contents를 가지고 있는 Loadable객체를 리턴한다.
state
: selector의 상태. 'hasValue', 'hasError', 'loading'의 값을 가짐.contents
: Loadable객체가 반환하는 값. 상태가 hasValue
이면 실제 값, 상태가 hasError
이면 오류가 생겼거나 상태가 loading인것. (이 때는 value가 Promise로 반환된다.)리코일 공식문서-비동기 데이터 쿼리
블로그-리코일 200% 활용하기
리코일문서 정리한 블로그글
블로그-Recoil레시피:비동기 액션
블로그-selector를 이용한 API 캐싱