Recoil은 페이스북이 2020년 5월에 소개한 React 전용으로 나온 상태 관리 라이브러리다.
Recoil을 통해 전역 상태를 관리하면 코드가 굉장히 간결해진다.
context API는 전역 상태를 전달할 때 객체 형태의 value를 사용한다.
객체 안의 값이 하나라도 변경되면 provider로 감싼 모든 하위 컴포넌트들이 리렌더링한다는 단점이 있다.
Recoil의 경우 각각의 전역 상태에 대한 atom이 생성되고 해당 상태를 구독하는 구성 요소만 리렌더링 된다.
따라서 불필요한 리렌더링을 방지할 수 있다.
Redux의 경우 안정적으로 상태 관리가 가능하다.
그럼에도 Redux에 대한 불만이 꾸준히 나오는 데에는 이유가 있을 것이다.
- React 전용 라이브러리가 아니다.
- 초기 세팅(boilerplate)이 요구된다.
- 비동기 데이터를 사용하려면 미들웨어 설치 등 추가적인 라이브러리 설치가 필요하다.
반면 Redux와 Mobx의 기능적인 문제는 없다. 하지만 이 상태 관리 라이브러리들은 리액트의 내부 라이브러리가 아니기 때문에 리액트의 가상돔의 내부 로직과는 별개로 동작한다. 그러니까 Redux의 store은 리액트의 상태와는 별개의 것이기 때문에 우리는 수많은 코드를 통해서 리액트의 상태와 리덕스의 상태를 일치시키는 작업을 해야 했다.
또한 가장 큰 단점은 리덕스의 단방향의 흐름은 상태를 디버깅하게 쉽게 해주지만 action, reducer, selector, store를 초기에 세팅하는 것은 엄청나게 번거로운 일이고 많은 코드를 추가하도록 강제한다. Redux Saga 이후 redux-toolkit이라는 라이브러리가 나와서 코드를 줄여주고 간단해지긴 했지만 그래도 초기에 세팅은 여전히 번거롭고 리액트와의 궁합은 만족스럽지 못하다.
Recoil은 이러한 단점을 보완한 라이브러리이다.
가장 큰 장점은
여러 컴포넌트에서 같은 Recoil 상태를 읽는 경우 첫 번째 컴포넌트가 상태 값을 읽고 캐싱하고, 다른 컴포넌트에서 동일한 상태를 읽어도 다시 계산하지 않습니다.
이는 상태의 불필요한 중복 계산을 방지하고 애플리케이션의 전반적인 성능을 향상시킵니다.
또한 엄청난 또다른 장점은 만약 데이터가 JSON 으로 들어와도 recoil 이 자동적으로 JSON 파싱 하여 데이터 유형으로 변환 해 줍니다.
Concurrent Mode (동시성 모드)
흐름이 여러개가 존재하는 경우를 의미한다.
즉, 리액트에서 알아서 렌더링 동작의 우선순위를 정하여 적절한 때에 렌더링을 해주는 것이다.
설치
npm i recoil
최상위 컴포넌트에서 감싸주기
import {RecoilRoot} from 'recoil';
root.render(
<RecoilRoot>
<App />
</RecoilRoot>
);
최상위 컴포넌트에서 감싸주어야 아톰이 최상위에서 다른 컴포넌트로 공유할 수 있기 때문입니다.
atom Hooks
1. useRecoilState
2. useRecoilValue
3. useResetRecoilState
import {atom} from 'recoil';
export const fontSizeState = atom({
key: 'fontSizeState',
default: 14
})
atom 의 특징
1. 동일한 atom 이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유합니다.
2. atom 의 key 값은 전역적으로 유일해야 합니다.
3. default 값은 배열, 함수, 객체던지 상관 없습니다.
이렇게 설정하고 각 컴포넌트에서 아톰을 불러와서 사용하면 됩니다.
사용법은 useState 와 똑같지만 다른 점은 상태가 컴포넌트 간에 공유될 수 있다는 것입니다.
컴포넌트에서 atom 을 불러올 때는 useRecoilState 를 사용해서 불러오면 됩니다.
import fontSizeState from './fontSizeState';
import { useRecoilState } from 'recoil';
const FontButton = () => {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button onClick={() => setFontSize(size => size + 1)} style={{fontsize}}>
Click me
</button>
)
}
다른 컴포넌트에서 값만 사용하고 싶을때는 useRecoilValue 를 사용하면 됩니다.
밑에 코드는 2가지 경우를 보여줍니다.
useRecoilValue 를 사용하는 경우
//useRecoilValue
const Text = () => {
const fontSizeText = useRecoilValue(fontSizeState);
return <p style={{fontSizeText}} >Text size<p>
}
useRecoilState 를 사용하는 경우
//useRecoilState
const Text = () => {
const [fontSizeText, setFontSizeText] = useRecoilState(fontSizeState);
return <p style={{fontSizeText}} >Text size<p>
}
Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)입니다.
상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행됩니다.
컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링 됩니다. 선택자를 사용하면 Recoil의 상태를 읽고 가공하여 새로운 데이터를 생성할 수 있습니다.
쉽게 생각하면 원래 내가 정해준 값을 가공해서 기존 값을 유지하면서 새로운 값을 보여주고 싶을 때 사용합니다.
그 외에 기존 값을 특정 이벤트가 발생했을때 바꾸고 싶다면
useRecoilState 를 이용해서 만든 두번 째 함수로 변경하면 됩니다.
중요한 것은 selector 은 종속성 값 집합에 대해 항상 동일한 값을 반환하는 부작용이 없는 "순수함수"이다.
순수 함수
항상 동일한 입력에 대해 동일한 출력을 반환합니다.
순수 함수
function add(a, b) {
return a + b;
}
비순수 함수
let counter = 0;
function impureFunction(value) {
counter++; // 외부 상태(counter)를 변경
return value * counter;
}
console.log(impureFunction(2)); // 첫 번째 호출: 2 * 1 = 2
console.log(impureFunction(2)); // 두 번째 호출: 2 * 2 = 4
console.log(impureFunction(2)); // 세 번째 호출: 2 * 3 = 6
useSeletor 사용 예시
import { atom, selector, useRecoilState, useRecoilValue, useSetRecoilState, RecoilRoot } from 'recoil';
import React from 'react';
const countState = atom({
key: 'countState',
default: 0,
});
const countPlusTwo = selector({
key: 'countPlusTwo',
get: ({get}) => {
const count = get(countState);
return count + 2;
}
});
function App() {
const count = useRecoilValue(countState);
const countValue = useRecoilValue(countPlusTwo);
return (
<div>
<p>Count: {count}</p>
<p>Count Plus Two: {countValue}</p>
</div>
);
}
function RecoilApp() {
return (
<RecoilRoot>
<App />
</RecoilRoot>
);
}
export default RecoilApp;
get
속성은 계산될 함수입니다.
전달되는 get
인자를 통해 atoms와 다른 selectors에 접근할 수 있습니다.
다른 atoms나 selectors에 접근하면 자동으로 종속 관계가 생성되므로,
참조했던 다른 atoms나 selectors가 업데이트되면 이 함수도 다시 실행됩니다.
set 은 selector 을 읽기 전용에서 쓰기 전용으로도 쓸 수 있게 사용할 수 있습니다.
import { atom, selector } from "recoil";
export const minuteState = atom({
key: "minutes",
default: 0,
});
export const hourSelector = selector<number>({
key: "hours",
get: ({ get }) => {
const minutes = get(minuteState) / 60;
return minutes;
},
set: ({ set }, newValue) => {
const minutes = Number(newValue) * 60;
set(minuteState, minutes);
},
});
import React from "react";
import { useRecoilState } from "recoil";
import { hourSelector, minuteState } from "./atoms";
function App() {
const [minutes, setMinutes] = useRecoilState(minuteState);
const [hours, setHours] = useRecoilState(hourSelector);
const onMinuteChange = (event) => {
setMinutes(+event.currentTarget.value);
};
const onHoursChange = (event) => {
setHours(+event.currentTarget.value);
};
return (
<div>
<input
type="number"
placeholder="Minutes"
value={minutes}
onChange={onMinuteChange}
/>
<input
type="number"
placeholder="Hours"
value={hours.toFixed(0)}
onChange={onHoursChange}
/>
</div>
);
}
export default App;
set 으로 설정해서 selector 을 이용해서 값을 바꾸면
selector set이 의존하는 atom의 값도 함께 변경될 수 있습니다.
즉 위에 코드에서는 시간을 바꾸면 분도 바꾸는 코드를 보여줍니다.
Selector를 이용한 비동기 요청 사용하기
Recoil의 Selector를 이용하면 비동기 액션에 대한 처리가 가능합니다.
export const userSelector = selector({
key: 'userSelector',
get: async () => {
const response = await UserService.getUser(1);
return response;
}
});
import React from 'react';
import { userSelector } from './states/userState';
import { useRecoilValue } from 'recoil';
function App() {
const user = useRecoilValue(userSelector);
return (
<>
{user}
</>
);
}
export default App;
하지만 이렇게 하고 불러 들이면 에러가 생깁니다. <Suspense fallback= .. 을
추가하라고 합니다.
Recoil에서는 비동기 호출시 발생하는 pending 상태에 대한 처리가 필요한데, React Suspense 또는 Loadable 객체를 이용한 방법이 있습니다.
React Suspense를 이용하여 비동기 호출에 대한 pending 상태를 처리하려면
비동기 호출이 발생하는 컴포넌트에 대해 <React.Suspense>로 감싸주어야 합니다.
ReactDOM.render(
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<App />
</React.Suspense>
</RecoilRoot>,
document.getElementById('root')
);
Loadable 객체는 atom 또는 selector의 상태를 나타냅니다.
import { useRecoilValueLoadable } from 'recoil';
import { userSelector } from './selectors';
function MyComponent() {
const myDataLoadable = useRecoilValueLoadable(userSelector);
switch (myDataLoadable.state) {
case 'loading':
return <div>Loading...</div>;
case 'hasValue':
const data = myDataLoadable.contents;
return <div>Data: {data}</div>;
case 'hasError':
const error = myDataLoadable.contents;
return <div>Error: {error.message}</div>;
default:
return null;
}
}
export default MyComponent;
Recoil 라이브러리에서 제공하는 React Hook 중 하나로,
Recoil 상태를 업데이트하거나 읽는 작업을 하나의 트랜잭션으로 묶을 때 사용됩니다.
예를 들어서 장바구니에 상품을 추가하려면 장바구니 상태와 재고 상태를 업데이트 해야 합니다.
만약에 장바구니 상태만 업데이트하고 예상치 못한 오류로 재고 상태는 업데이트 하지 못하는 경우가 생기면
예기치 못한 상태를 유발할 수 있기 때문에 useRecoilTransaction 은 하나라도 업데이트가 실패하면
두가지 모두 실패하게끔 만들어 줄 수 있습니다.
또한 업데이트는 동시에 실행됩니다.
import { useRecoilState, useRecoilTransaction_UNSTABLE } from 'recoil';
function AddToCartButton({ product }) {
const [cart, setCart] = useRecoilState(cartState);
const [inventory, setInventory] = useRecoilState(inventoryState);
const handleAddToCart = () => {
useRecoilTransaction_UNSTABLE(() => {
// 장바구니 상태 업데이트
setCart([...cart, product]);
// 재고 상태 업데이트
setInventory({ ...inventory, [product.id]: inventory[product.id] - 1 });
});
};
return (
<button onClick={handleAddToCart}>Add to Cart</button>
);
}
useRecoilRefresher_UNSTABLE()
은 연관된 모든 캐시를 삭제하고
selector를 호출할 수 있는 콜백을 반환합니다.
selector가 비동기 요청을 하면 재평가하고 새롭게 요청을 합니다.
예를 들어, 최신 데이터로 업데이트하거나 오류가 나서 다시 시도하는 경우에 유용합니다.
atom은 새로고침되지 않으며, 현재 상태를 유지합니다.
selectors는 캐쉬를 지웁니다.
selector는 주로 추상화로 사용하기 때문에 selector를 새로 고치면
연관된 모든 selector의 캐시가 재귀적으로 새로 고쳐집니다.
const myQuery = selector({
key: 'MyQuery',
get: () => fetch(myQueryURL),
});
function MyComponent() {
const data = useRecoilValue(myQuery);
const refresh = useRecoilRefresher_UNSTABLE(myQuery);
return (
<div>
Data: {data}
<button onClick={() => refresh()}>Refresh</button>
</div>
);}