recoil은 애플리케이션 내에서 공유되는 상태를 효과적으로 관리하기 위해 데이터 플로우 그래프
를 사용해서 상태를 정의하며, 이러한 상태들을 리액트 컴포넌트에 제공하여 컴포넌트 간의 상태 공유를 용이하게 한다.
여기서 이야기하는 데이터 플로우 그래프
에 대한 설명은 다음과 같다.
상태와 그 상태를 구독하는 컴포넌트 간의 의존 관계를 시각적으로 나타낸 것을 의미
recoil의 상태는 atom
이라고 불리는 단위로 정의되고, atom
들은 서로 연결되어 있는 데이터 플로우 그래프를 형성하고, selector
를 이용해 파생된 상태는 데이터 플로우 그래프에서 파생되는 상태를 의미한다. recoil은 이러한 의존성을 자동으로 추적하고, 상태가 업데이트 될 때마다 파생된 상태를 다시 계산해서 최신 값을 유지하는 것이다.
여기서 중요한 것은 selector
를 이용해서 비동기 데이터를 데이터 플로우 그래프로 포함시키는 것이 가능하다는 점이다. selector
는 순수 함수를 대표하고 있다. 즉, 주어진 인풋으로 항상 같은 결과를 만들어 낼 수 있기 때문에 DB 쿼리를 모델링하는데 좋은 방법으로 사용되는 이유이다.
selector
를 이용해 데이터 베이스에 저장되어 있는 데이터를 호출하기 위해서는 Promise
를 반환하거나 async
함수를 사용해서 결과를 전달하면된다. 여기서 selector
의 의존성에 하나라도 변경이 생기면 새로운 쿼리를 평가하고 재실행시킬 것이다. 또한, 결과는 쿼리가 이전 인풋 데이터와 다른 경우에만 실행되도록 캐싱된다.
// 상태 정의
const userIdState = atom({
key: 'userIdState',
default: 2,
});
const currentUserNameQuery = seletor({
key: 'currentUserNameQuery',
get: async ({get}) => {
const response = await getUserName({
userId: get(userIdState),
});
return response.data;
}
});
// 호출
function TestComponent () {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
// 상태 정의
const currentUserNameQuery = seletor({
key: 'currentUserNameQuery',
get: async (userId) => {
const response = await getUserName({userId});
return response.data;
}
});
// 호출
function TestComponent ({userId}) {
const userName = useRecoilValue(currentUserNameQuery(userId));
return <div>{userName}</div>;
}
selector
를 통해 비동기 데이터 호출을 위와 같이 수행했을 때 한가지 고려해야 할 부분이 있다. 리액트의 렌터 함수가 동기로 실행되는데 Promise
가 resolve
되기 전에 컴포넌트는 어떤 것을 렌더링 할 수 있으며, 무엇을 렌더링 해야할까? 위 코드만 실행을 시켰을 경우에는 아래와 같은 경고가 발생한다.
이런 문제점을 해결하기 위해서는 어떻게 해야할까?
recoil은 보류중인 데이터를 다루기 위해서는 Reqct Suspense
와 함께 동작하도록 디자인 되어 있다. Suspense
는 하위 항목이 로드가 완료될 때까지 대체 컴포넌트를 표시할 수 있는 API이다.
다음과 같은 경우에 사용된다.
function TestComponent (){
return (
<RecoilRoot>
<Suspense fallback={<div>Loading...</div>}>
<App/>
</Suspense>
</RecoilRoot>
);
}
suspense
는 렌더링하려는 실제 UI를 로드가 완료되지 않은 경우 fallback
으로 지정된 컴포넌트로 대체하면서 데이터의 준비를 도와주는 API라고 보면 편하다.
이렇게 suspense
를 사용해야 Promise
가 resolve
된 후 데이터를 정상적으로 출력 할 수 있게 된다. 만약 suspense
를 사용하는 것이 싫다면 react-query
등을 사용하여 서버 데이터를 가져온 이후 원하는 시점에 상태로 업데이트하여 사용하는 등의 방법을 사용해보자.
function SomeComponent () {
// 데이터 호출
const someData = useQuery(getData());
const [someState, setSomeState] = useRecoilState(someState);
useEffect(() => {
// api 데이터의 응답 값이 오면 state 업데이트
if(someData){
setSomeState(someData);
}
}, [someData]);
return (
...생략
);
}
selector를 이용해 컴포넌트에서 데이터를 호출할 대 반환되는 데이터가 컴포넌트의 렌더링 이후에 발생한다면 위와 같은 에러가 발생한다. 이러한 문제점을 해결하기 위한 두번째 방법으로는 recoil의 useRecoilValueLoadable
훅을 사용하는 것이다.
useRecoilValueLoadable
훅은 loadable 객체를 반환하는데 해당 객체에는 contents
와 state
상태가 포함되어 있다. 여기서 state
는 다음 세가지 상태로 분류된다.
hasValue
: 데이터 로드가 완료되어 데이터를 가지고 있는 상태loading
: 데이터를 로드하기 위해 진행중인 상태hasError
: 데이터 로딩 중 에러가 발생한 상태위 세가지 상태를 이용하여 조건부 렌더링을 수행하면 컴포넌트가 렌더링 된 이후 데이터가 로드되어 발생하는 에러를 차단할 수 있다.
// 사이드 메뉴 생성
const SideList = () => {
// loadable 객체 호출
const loadableDomains = useRecoilValueLoadable(sideMenuState.sideManus);
// 상태를 변수로 대입
const state = loadableDomains.state;
return (
<SideListBox>
<SideUl>
// 데이터를 가지고 있는 상태라면 컴포넌트 렌더링
{state === "hasValue" &&
loadableDomains.contents.map((domain) => (
<li key={domain.name} className="side-title">
<div className="title-box">{domain.name}</div>
<SideCategory categories={domain.categories} />
</li>
))}
</SideUl>
</SideListBox>
);
};
먼저 loadable 객체를 호출하고 상태를 변수로 받는다. 해당 변수의 상태 변화에 따라 렌더링 조건을 부여해주면 되는데 나는 위 코드에서 처럼 데이터를 가지고 있는 경우에만 렌더링 되도록 조건을 1개만 부여하였다. 필요하다면 로딩 상태를 나타내는 컴포넌트 등을 부여해주면 좋다.
Suspense
와 loadable
은 특징이 다르기 때문에 사용할 때에 해당 컴포넌트가 동작하는 범위와 역할을 잘 고려하여 적용해야한다. Suspense
같은 경우는 컴포넌트를 감싸서 조금 더 넓은 범위에서 적용이 되며, loadable
의 경우에는 해당 recoil 상태에 국한되어 적용되기 때문에 이 부분을 잘 고려해보자.