2022.03.08 - [Javascript/React] - Recoil - 상태관리 라이브러리
이전에 단순히 recoil을 단순히 전역 상태만으로 사용해서(그조차도 맞는지 모르는...) 조금은 부족하다는 생각에 recoil을 좀 더 상세히 다루어보려고 한다.
상태 관리의 중요성
양방향 바인등을 하는 Angular와는 달리 React는 단방향으로 바인딩 한다. 상위 -> 하위 방향으로만 state를 props로 전달할 수 있고, 하위의 props를 상위로 전달하는 방법은 존재하지 않는다.
대신 하위 component에서 상위 component의 state를 바꿀 수 있는 두 가지 방법이 있다.
1. 자식에게 부모의 state를 변경할 수 있는 setState함수를 props로 넘겨준다.
2. state management tool을 사용한다.(recoil, redux ...)
1번의 경우 프로젝트의 규모가 커질수록 상태 관리가 어려워진다. depth가 조금만 깊어지면 1번의 방법은 효율적이지 않게된다.
그래서 2번과 같은 state management tool이 나오게 됐다.
관련 글
2022.07.04 - [Javascript/React] - [React] React 상태 관리
Recoil의 기본 Atom
An atom represents state in Recoil. The atom() function returns a writable RecoilState object.
- recoil 공식문서 -
atom은 상태의 단위이다.
atom이 업데이트 되면, 해당 atom을 구독하고 있던 모든 컴포넌트들의 state가 새로운 값으로 리렌더 된다.
unique한 id인 key로 구분되는 각 atom은 여러 컴포넌트에서 atom을 구독하고 있다면 그 컴포넌트들도 똑같은 상태를 공유한다.
사용방법은 이전 포스팅을 참고해주세요.
Selector와 비동기 처리(Suspense, Loadable)
이전 포스팅에서 많이 부족했던 Selector에서 더 알아보자.
프로젝트에 추가해보려고 했는데, 공식문서랑 구글링을 아무리 해봐도 잘 이해가 되지 않아서 좀 더 자세히 정리해보려고한다.
A selector represents a piece of derived state
- recoil 공식문서 -
atom만을 사용해서는 비동기 처리를 할 수 없다.
selector는 공식문서의 설명과 같이 파생된 state를 저장하고 있다.
export const wonilState = atom({
key: 'wonilState',
default: []
});
export const getWonilSelector = selector({
key: 'wonilState',
get: async ({ get }) => {
try {
const {data} = await client.get('/wonil');
return data.data;
} catch (error) {
throw error;
},
},
set: ({ set }, newValue) => {
set(wonilState, newValue)
}
});
다음과 같이 비동기 로직을 selector에서는 한 번에 처리 할 수 있다. 주의해야할 점은 selector는 *순수함수여야 한다는 것이다.
*순수함수: 같은 입력이 들어오면, 해당 입력에 대한 출력은 항상 같은 함수라는 뜻
공식문서의 selector의 타입을 보자
function selector<T>({
key: string,
get: ({
get: GetRecoilValue
}) => T | Promise<T> | RecoilValue<T>,
set?: (
{
get: GetRecoilValue,
set: SetRecoilState,
reset: ResetRecoilState,
},
newValue: T | DefaultValue,
) => void,
dangerouslyAllowMutability?: boolean,
})
key: selector를 구분할 수 있는 유일한 id
get: derived state를 반환하는 곳
set: writeable 한 state 값을 변경할 수 있는 함수를 반환하는 곳이다.
여기서 주의할 점은, 자기 자신 selector를 set 하려고하면, 스스로를 해당 set function에서 set하는 것이므로 무한루프가 돌게된다.
따라서 반드시 다른 selector와 atom을 set하는 로직을 구성해야한다. 또한 selector는 read-only한 return 값(RecoilValue)만 가지기 때문에 set으로는 writeable한 atom의 RecoilState만 설정할 수 있다.
selector는 read-only한 return값만 가진다고 했다.
근데 여기서 useRecoilState()는 setCookie()라는 state를 변경할 수 있는 set 함수도 반환하고 있다.
위의 set 설명에서 언급했듯, 이는 writeable한 state 즉, atom의 값만 수정할 수 있다.
따라서, setState()함수는 selector의 값을 수정하는 함수가 아닌, 다른 atom의 writeable한 state를 수정할 수 있는 함수를 정의한 경우에만 사용할 수 있다.
selector는 결국 다른 selector나 atom의 값을 get해올 수 있고, get해온 값을 바탕으로 다른 atom의 state를 설정할 수 있는 역할을 한다.
Recoil의 Loadable
selector에서 비동기 처리를하고나면, 비동기를 처리하고 있을 때(=렌더링 할 데이터가 아직 도착하기 이전, loading) 보여줄 fallback UI가 없으면 에러가 뜨므로 loading 처리를해야 한다.
Recat에서 제공하는 React.Suspense를 통해서 처리할 수도 있지만 Recoil의 Loadable을 사용할 수도 있다.
비동기 처리 한 selector를 만들었다 가정한다.
import { getWonilSelector } from '../recoil';
import { useRecoilState, useRecoilValueLoadable } from 'recoil';
const Wonil = () => {
const wonilLoadable = useRecoilValueLoadable(getWonilSelector);
switch(wonilLoadable.state){
case 'hasValue':
return (
<>
<div>
{wonilLoadable.contents.map(
...
)}
</div>
</>
)
case 'loading':
return <Loading />;
case 'hasError':
throw wonilLoadable.contents;
}
}
export default Wonil;
Loadable은 atom이나 selector의 현재 상태를 나타내는 객체이다.
state: hasValue, hasError, loading - atom이나 selector의 상태를 말하며, 세 가지 상태를 가질 수 있다.
contents: atom이나 contents의 값을 나타내며, 상태에 따라 다른 값을 가지고 있다.
hasValue 상태일 땐 value를, hasError 상태일 땐 Error 객체를, loading 상태일 땐 Promise를 가지고 있다.
selector를 통한 성능 개선 - 캐싱
selector는 기본적으로 값을 캐싱한다. 들어왔던 적이 있는 값을 기억하고 있기 때문에, 같은 응답을 보내는 api call에 대해서는 추가적으로 요청하지 않아 성능적으로 유리하다.
동적인 url에 대한 api call - selectorFamily
외부에서 파라미터로 값을 받아와서 selector에 적용해야 할 경우 selectorFamily를 사용한다.
api콜을 할 때 요청을 보내는 서버의 url은 다음과 같이 params를 받는 경우가 많은데, 이런 경우에는 selectorFamily를 이용하면 해결할 수 있다.
'https://baseUrl.com/type=${apiTypes}'
github 레포지토리를 읽어오는 github이 제공하는 api이다. githubId를 동적으로 받아와야할 경우에 selectFamily를 사용하면 된다.
export const githubRepo = selectorFamily({
key: "github/get",
get: (githubId) => async () => {
if (!githubId) return "";
const { data } = await axios.get(
`https://api.github.com/repos/${githubId}`
);
return data;
},
});
import { useRecoilValue } from 'recoil';
import { selectorFamily } from '../../state';
const Github = () => {
const githubId = 'wonillism';
const githubRepos = useRecoilValue(githubRepo(githubId));
return(
<>
<div>Repos : {githubRepos}</div>
</>
)
}
export default Github;
https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0