Recoil 이란 ?
ecoil 은 페이스북에서 만든 상태 관리 라이브러리이고 2020년 5월에 진행된 React Europe 2020 에서 처음으로 소개되었습니다.
현재 (2021년 8월 2일) 기준으로 최신 버전이 0.4.0 이고 아직까진 프로덕션에 사용되기는 이를 수도 있지만, Redux, MobX 등 기존의 상태 관리 라이브러리들을 대체할 다양한 장점이 존재하므로 점차 발전할 것으로 기대됩니다.
1. Recoil vs Redux
액션, 리듀서, 미들웨어 등 boilerplate 코드가 많이 발생하는 Redux 와는 대조적으로 Recoil 은 boilerplate-free API 제공한다. React 의 useState 처럼 간단한 게터(get) / 세터(set) 인터페이스로 사용 가능합니다.
Redux 의 상태 구조는 트리 구조를 따르지만 Recoil 은 방향 그래프(directed graph, digraph) 를 따릅니다.
Recoil 은 상태를 사용하는 컴포넌트를 수정하지 않고 파생 데이터(derived data)를 대체할 수 있습니다.
기본적으로 아톰(atom)의 데이터가 변경되면 해당 atom 을 구독하는 모든 컴포넌트들은 갱신됩니다. Redux 에서는 해당 기능을 수행하기 위해 reselect 같은 3rd-party 라이브러리가 필요합니다.
AtomEffect 를 사용해서 특정 상태의 갱신 이후의 사이드 이펙트를 자체적으로 정의 가능합니다
- 상태 갱신 이후에 영향받는 컴포넌트에서 직접 useEffect를 사용할 필요가 없다.
2. Atom
Recoil 의 상태 단위입니다.
스토어에 저장되고 갱신되는 데이터는 모두 Atom을 기반으로 합니다.
아톰이 갱신될 때 그 상태를 구독(subscribe) 하고 있는 컴포넌트는 새로운 값으로 리렌더 됩니다.
아톰은 atom() 함수에 key 와 default 을 전달해서 작성합니다.
3. Selectors
Selector 는 상태를 기반으로 전달된 데이터를 가공할 때 사용됩니다.
selector() 함수에 key 와 get 와 set 를 전달하여 작성합니다.
import { selector } from 'recoil';
...
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
get 프로퍼티는 계산에 사용되는 함수입니다.
전달된 get 인수를 사용해서 아톰(Atom)이나 다른 셀렉터(Selector)에 접근할 수 있으며, 접근한 아톰이나 셀렉터가 업데이트 되면 다시 계산됩니다.
위 예제의 셀렉터는 fontSizeState 상태를 가져와 폰트 사이즈를 출력하는 순수 함수처럼 동작합니다.
셀렉터는 쓸 수(write)없기 때문에 useRecoilState를 사용하지 않고 useRecoilValue를 사용합니다.
import { useRecoilState, useRecoilValue } from 'recoil';
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeLabel = useRecoilValue(fontSizeLabelState);
return (
<>
<div>Current font size: ${fontSizeLabel}</div>
<button onClick={setFontSize(fontSize + 1)} style={{fontSize}}>
Click to Enlarge
</button>
</>
);
}
4. 비동기 데이터 쿼리
Redux 에선 비동기 데이터 쿼리를 위해 redux-thunk, redux-observable, redux-saga 등의 Middleware 를 사용해야 합니다.
Middleware 는 action 을 dispatch 하고 reducer 에서 상태 업데이트를 하기 전 비동기 처리(예: 네트워크 요청, setTimeout)를 하는 중간자 역할이라고 보면 됩니다.
Redux Middleware 는 비동기 처리를 위해 여전히 강력한 도구지만 이를 위해 추가되는 boilerplate 코드가 많아지는 것은 무시할 수 없습니다.
당연히도 앱의 규모가 커질 수록 복잡도가 늘어나고 코드 양이 더욱 방대해집니다.
이와는 다르게 Recoil 에선 동기 / 비동기 함수 모두 selector 에서 처리할 수 있습니다.
단, React 의 render() 함수가 동기이기 때문에 promise 가 resolve 되기 전에 렌더링 할 수가 없습니다.
이때 대기중인 데이터를 처리하기 위해 Recoil 은 React Suspense 와 함께 사용되도록 디자인되어있습니다.
아래의 예처럼 컴포넌트를 Suspense 로 감싸서 대기중인 하위 항목들을 잡아내고 fallback UI 를 대신 렌더합니다.
// atom.js
export const todoIdState = atom({
key: "todoIdState",
default: 1
});
export const todoItemQuery = selector({
key: "todoItemQuery",
get: async ({ get }) => {
const id = get(todoIdState);
const response = await axios.get(
`https://jsonplaceholder.typicode.com/todos/${id}`
);
return response.data;
}
});
// App.js
import { RecoilRoot } from "recoil";
import { Suspense } from "react";
import Container from "./container";
export default function App() {
return (
<RecoilRoot>
<Suspense fallback={() => <p>Loading...</p>}>
<Container />
</Suspense>
</RecoilRoot>
);
}
// container/index.js
import { todoItemQuery } from "../atom";
import { useRecoilValue } from "recoil";
const Container = () => {
const data = useRecoilValue(todoItemQuery);
return <div>{data.title}</div>;
};
export default Container;
파라미터에 따라 비동기 데이터 요청
파라미터를 기반으로 쿼리하고 싶을 땐 selectorFamily 를 사용할 수 있습니다.
// atom.js
import axios from 'axios';
import { selectorFamily } from 'recoil';
export const todoItemQuery = selectorFamily({
key: "todoItemQuery",
get: (id) => async () => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/todos/${id}`
);
return response.data;
}
});
// App.js
import { RecoilRoot } from "recoil";
import { Suspense } from "react";
import Container from "./container";
export default function App() {
return (
<RecoilRoot>
<Suspense fallback={() => <p>Loading...</p>}>
<Container id={1} />
</Suspense>
</RecoilRoot>
);
}
// container/index.js
import { todoItemQuery } from "../atom";
import { useRecoilValue } from "recoil";
const Container = ({ id }) => {
const data = useRecoilValue(todoItemQuery(id));
return <div>{data.title}</div>;
};
export default Container;
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다
댓글