Recoil은 페이스북에서 만든 새로운 React를 위한 상태 관리 라이브러리이다. Recoil을 사용하면 atoms (공유 상태)에서 selectors (순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있습니다.
Atoms는 컴포넌트가 구독할 수 있는 상태의 단위이며 Selectors는 atoms 상태 값을 동기 또는 비동기 방식을 통해 변환해줍니다.
적은 코드양, 쉬운 러닝 커브
Recoil은 상태 하나를 atom으로 정의하고, 그것을 구독하는 패턴이다. 또한 이런 atom 들로부터 파생된 상태를 selector로 선언한다. 이 selector 들은 구독한 atom이 변화하면 자동으로 변화한다. 그리고 페이스북에서 만든 만큼 리액트와도 매우 잘 어울려서 훅을 사용해보았다면 쉽게 배울 수 있다.
렌더링 최적화
Context에서는 상태 변경 시 구독한 하위 컴포넌트들이 모두 리렌더링 되는 문제가 있었는데, Recoil은 atom, selector를 구독하면 구독한 컴포넌트만 리렌더링이 일어난다.
간편한 비동기 처리
Recoil에서는 자체적으로 selector에서 지원한다. 에러처리도 리액트의 suspense
를 사용할 수 있고, useRecoilValueLoadable
을 사용한다면, hasValue, loading, hasError 상태를 제공해서, 로딩 완료 시, 로딩 시, 에러 시 분기 처리가 손쉽게 가능하다.
리코일 state를 사용하는 컴포넌트들은 <RecoilRoot>
를 필요로 한다. <RecoilRoot>
를 사용하는 가장 좋은곳은 root component이다.
// app.js
import React from 'react';
import {RecoilRoot} from 'recoil';
import Counter from './Counter';
const App = () => {
return (
<RecoilRoot>
<Counter />
</RecoilRoot>
);
};
아톰은 상태를 말하며 어떠한 컴포넌트에서 씌여지고 읽혀질 수 있다. 아톰의 value를 읽는 컴포넌트들은 암묵적으로 그 아톰을 구독하고 있다. 그래서 아톰을 업데이트하면 모든 컴포넌트가 리렌더링 되는게 아니라 아톰을 참고하고 있는 컴포넌트만 리렌더링 된다.
//countAtom.js
export const countState = atom({
key: 'countState', // 해당 atom의 고유 key
default: 0, // 기본값
});
selector 는 atom 의 상태에 의존하는 동적인 데이터를 생성 생성한다. selector 에서는 get 함수(필수항목)를 통해 atom 정보들을 1개이상 가져올 수 있다. 이를 통해 atom을 조합하여 간단히 새로운 데이터를 생성할수 있다. 물론 atom 의 정보가 바뀌면 해당 atom 을 의존하는 selector 도 자동으로 리렌더링이 된다. 또한 한개 이상의 atom정보를 업데이트 하도록 set 함수(선택항목)를 받을 수 있다.
//countAtom.js
export const countState = atom({
key: 'countState', // 해당 atom의 고유 key
default: 0, // 기본값
});
export const countInputValueState = selector({
key: 'countInputValueState',
get: ({ get }) => {
const countValue = get(countState);
return `현재 카운트는 ${countValue} 입니다.`;
},
});
useRecoilState
useRecoilValue
useSetRecoilState
useResetRecoilState
useRecoilValueLoadable
// App.jsx
import { RecoilRoot } from 'recoil';
import Counter from './Counter';
const App = () => {
return (
<RecoilRoot>
<Counter />
</RecoilRoot>
);
};
export default App;
// countAtom.js
import { atom, selector } from 'recoil';
export const countState = atom({
key: 'countState',
default: 0,
});
export const countInputValueState = selector({
key: 'countInputValueState',
get: ({ get }) => {
const countValue = get(countState);
return `현재 카운트는 ${countValue}입니다.
`;
},
});
// Counter.jsx
const Counter = () => {
const [countValue, setCounterValue] = useRecoilState(countState);
const resetCountValue = useResetRecoilState(countState);
const resultValue = useRecoilValue(countInputValueState);
const plusCount = () => setCounterValue((prev) => prev + 1);
const minusCount = () => setCounterValue((prev) => prev - 1);
return (
<div>
<div>{countValue}</div>
<button onClick={plusCount}>+</button>
<button onClick={minusCount}>-</button>
<button onClick={resetCountValue}>reset</button>
</div>
);
};
export default Counter;
// App.jsx
import { RecoilRoot } from 'recoil';
import Counter from './Counter';
const App = () => {
return (
<RecoilRoot>
<Counter />
</RecoilRoot>
);
};
export default App;
// countAtom.js
import { atom, selector } from 'recoil';
export const countState = atom({
key: 'countState',
default: 0,
});
export const inputCountState = atom({
key: 'inputCountState',
default: 0,
});
export const countInputValueState = selector({
key: 'countInputValueState',
get: ({ get }) => {
const countValue = get(countState);
const inputValue = get(inputCountState);
return `현재 카운트는 ${countValue}입니다, 입력 카운트는 ${inputValue} 입니다`;
},
});
// Counter.jsx
import { countState, inputCountState, countInputValueState } from './countAtom';
import { useRecoilState, useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
const Counter = () => {
const [countValue, setCounterValue] = useRecoilState(countState);
const resetCountValue = useResetRecoilState(countState);
const inputCountValue = useRecoilValue(inputCountState);
const setInputCountValue = useSetRecoilState(inputCountState);
const resultValue = useRecoilValue(countInputValueState);
const plusCount = () => setCounterValue((prev) => prev + 1);
const minusCount = () => setCounterValue((prev) => prev - 1);
const inputHandler = ({ target }) => setInputCountValue(target.value);
const submitCount = () => setCounterValue((prev) => prev + Number(inputCountValue));
return (
<div>
<div>{countValue}</div>
<div>
<button onClick={plusCount}>+</button>
<button onClick={minusCount}>-</button>
<button onClick={resetCountValue}>reset</button>
</div>
<div>
<input type="text" placeholder={inputCountValue} onChange={inputHandler} />
<button onClick={submitCount}>입력값 더하기</button>
</div>
<div>{resultValue}</div>
</div>
);
};
export default Counter;
// App.jsx
import { RecoilRoot } from 'recoil';
import SelectorCount from './StarCounter';
const App = () => {
return (
<RecoilRoot>
<SelectorCount />
</RecoilRoot>
);
};
export default App;
// starAtom.js
import { selector } from 'recoil';
export const starCountState = selector({
key: 'starCountState',
get: async () => {
const response = await fetch('https://api.github.com/repos/facebookexperimental/Recoil');
const recoilProjectInfo = await response.json();
// stargazers_count 반환
return recoilProjectInfo['stargazers_count'];
},
});
// SelectorCount.jsx
import { useRecoilValueLoadable } from 'recoil';
import { starCountState } from './starAtom';
const SelectorCount = () => {
const starCount = useRecoilValueLoadable(starCountState);
// 로딩 상태 처리
if (starCount.state === 'loading') return <div>loading</div>;
return (
<>
<p>recoil github star 갯수 </p>
<p>{starCount.contents}개</p>
</>
);
};
export default SelectorCount;