Recoil vs Redux
Redux
- 액션, 리듀서, 미들웨어 등 boilerplate 코드가 많이 발생함
- Redux 의 상태 구조는 트리 구조를 따름
- Redux 에서는 reselect 같은 3rd-party 라이브러리가 필요
Recoil
- Recoil 은 boilerplate-free API 를 제공함.
- React 의 useState 처럼 간단한 게터(get) / 세터(set) 인터페이스로 사용 가능
- Recoil 은 방향 그래프(directed graph, digraph) 를 따름
- Recoil 은 상태를 사용하는 컴포넌트를 수정하지 않고 파생 데이터(derived data)를 대체할 수 있음
- 기본적으로 아톰(atom)의 데이터가 변경되면 해당 atom 을 구독하는 모든 컴포넌트들은 갱신
- AtomEffect 를 사용해서 특정 상태의 갱신 이후의 사이드 이펙트를 자체적으로 정의 가능
- 상태 갱신 이후에 영향받는 컴포넌트에서 직접 useEffect를 사용할 필요가 없음
Atom / Selector
Atom
- recoil 의 상태 단위
- store에 저장되고 갱신되는 데이터는 모두 Atom을 기반
- 아톰이 갱신될 때 그 상태를 구독(subscribe) 하고 있는 컴포넌트는 새로운 값으로 리렌더
- 아톰은 atom() 함수에 key 와 default 을 전달해서 작성
- 활용 사례 : 최초 로그인 시, accessToken을 atom에 저장하여, 다른 컴포넌트(사이드 바, 마이페이지 등)에서 사용할 수 있었다.
Selector
- 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>
</>
);
}
- 활용 사례 : 유저의 accesstoken이 만료된 것을 파악하고, restore하며 새로운 accesstoken을 발급받을 때, 활용했었다.
비동기 데이터 쿼리
Redux
- redux-thunk, redux-observable, redux-saga 등의 Middleware 사용
- Middleware : action 을 dispatch 하고 reducer 에서 상태 업데이트를 하기 전 비동기 처리(예: 네트워크 요청, setTimeout)를 하는 중간자 역할
- Redux Middleware를 위해 추가되는 boilerplate 코드가 많아짐 + 앱의 규모가 커질 수록 복잡도가 늘어나고 코드 양이 더욱 방대해짐
Recoil
- 동기 / 비동기 함수 모두 selector 에서 처리
- render() 함수가 동기이기 때문에 promise 가 resolve 되기 전에 렌더링 할 수가 없음
- 이때 대기중인 데이터를 처리하기 위해 Recoil 은 React Suspense 와 함께 사용
- 컴포넌트를 Suspense 로 감싸서 대기중인 하위 항목들을 잡아내고 fallback UI 를 대신 렌더링
/ store.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 "../store";
import { useRecoilValue } from "recoil";
const Container = () => {
const data = useRecoilValue(todoItemQuery);
return <div>{data.title}</div>;
};
export default Container;
파라미터에 따라 비동기 데이터 요청
- 파라미터를 기반으로 쿼리하고 싶을 땐 selectorFamily 를 사용
// store.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={<div>Loading...</div>}>
<Container id={1} />
</Suspense>
</RecoilRoot>
);
}
// container/index.js
import { todoItemQuery } from "../store";
import { useRecoilValue } from "recoil";
const Container = ({ id }) => {
const data = useRecoilValue(todoItemQuery(id));
return <div>{data.title}</div>;
};
export default Container;