Recoil
2020년도 전 세계의 프론트엔드 개발자 4500명을 대상으로한 설문에 의하면 3년 안에 사라질 것 같은 솔루션 1위로 Redux가 뽑혔다. 조사에 의하면 이미 상태 관리를 위해 React Context API 및 Hooks를 사용하는 비율이 Redux를 추월하였다. 앞으로 React의 상태관리는 Context API로 대체되는 것일까? 이러한 상황에서 2020년 5월 FaceBook에서 Recoil이라는 상태관리 솔루션을 발표하였다. Recoil을 한 번 사용해보자.
내가 Recoil로 만들 것은 버튼을 누르면 카운트가 증가하거나 감소 혹은 0으로 리셋되는 간단한 카운터이다.
1. 초기 세팅
import React from 'react';
import { RecoilRoot } from 'recoil';
import Counter from './Counter';
import './App.css';
function App() {
return (
<RecoilRoot>
<Counter/>
</RecoilRoot>
);
}
Recoil이 가진 상태를 사용하는 컴포넌트(Counter)는 상위 트리에 RecoilRoot가 필요하다. App.js에 앞으로 만들 Count 컴포넌트를 넣고 RecoilRoot로 감싸주자.
import React from 'react';
function Counter() {
return (
<div>
<h1>Counter</h1>
<span id="value">{count}</span>
<div class="button-container">
<button class="btn-increase" >increase</button>
<button class="btn-decrease" >decrease</button>
<button class="btn-reset" >reset</button>
</div>
</div>
);
}
export default Counter;
Counter 컴포넌트의 html 요소들을 작성한다.
2. Atom과 useRecoilState
const countState = atom({
key: 'countState',
default: 0
});
Recoil로 관리하고 싶은 값(State)은 Atom이라는 단위로 만든다. Atoms은 어떤 컴포넌트에서나 읽고 쓸 수 있으며 atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 구독한다. atom은 고유의 key와 초기 값으로 구성되어있다.
import React from 'react';
import { atom, useRecoilState } from 'recoil';
const countState = atom({
key: 'countState',
default: 0
});
function Counter() {
const [count, setCount] = useRecoilState(countState);
const increaseCount = () => {
setCount(count + 1);
};
const decreaseCount = () => {
setCount(count - 1);
};
return (
<div>
<h1>Counter</h1>
<span id="value">{count}</span>
<div class="button-container">
<button class="btn-increase" onClick={increaseCount}>increase</button>
<button class="btn-decrease" onClick={decreaseCount}>decrease</button>
<button class="btn-reset">reset</button>
</div>
</div>
);
}
atom으로 등록된 값은 useRecoilState()를 이용하여 읽고 쓸 수 있다. useRecoilState(atom)를 통해 상태 값과 상태 값을 변경하는 함수를 리턴받아 사용하는 것이다. 리액트 훅스를 써보았다면 useState와 매우 유사하다는 생각이 들 것이다.
3. useResetRecoilState
import React from 'react';
import { atom, useRecoilState, useResetRecoilState } from 'recoil';
/*countState 코드 생략*/
function Counter() {
const [count, setCount] = useRecoilState(countState);
const resetCount = useResetRecoilState(countState);
const increaseCount = () => {
setCount(count + 1);
};
const decreaseCount = () => {
setCount(count - 1);
};
return (
<div>
<h1>Counter</h1>
<span id="value">{count}</span>
<div class="button-container">
<button class="btn-increase" onClick={increaseCount}>increase</button>
<button class="btn-decrease" onClick={decreaseCount}>decrease</button>
<button class="btn-reset" onClick={resetCount}>reset</button>
</div>
</div>
);
}
카운터의 값을 리셋할 때는 setCount에 0을 넣어도 되지만 Recoil에서는 useResetRecoilState()라는 함수를 제공한다. 개발자가 0이라 지정하지 않아도 상태 값을 초기에 지정한 default 값으로 변경해준다.
4. useRecoilValue와 useSetRecoilState
import React from 'react';
import { atom, useResetRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
/*countState 코드 생략*/
function Counter() {
const count = useRecoilValue(countState);
const setCount = useSetRecoilState(countState);
const resetCount = useResetRecoilState(countState);
const increaseCount = () => {
setCount((preCount) => preCount + 1);
};
const decreaseCount = () => {
setCount((preCount) => preCount - 1);
};
return (
<div>
<h1>Counter</h1>
<span id="value">{count}</span>
<div class="button-container">
<button class="btn-increase" onClick={increaseCount}>increase</button>
<button class="btn-decrease" onClick={decreaseCount}>decrease</button>
<button class="btn-reset" onClick={resetCount}>reset</button>
</div>
</div>
);
}
useRecoilState()는 상태 값과 상태 값을 변경하는 함수를 모두 리턴했다면, useRecoilValue()는 상태 값만, useSetRecoilState()는 상태 값을 변경하는 함수만 가져올 수 있다. 즉, useRecoilState() 대신 위와 같이 useRecoilValue(), useSetRecoilState()를 사용하는 코드로 변경 가능하다.
5. selector
import React from 'react';
import { atom, useResetRecoilState, useRecoilValue, useSetRecoilState, selector } from 'recoil';
const countState = atom({
key: 'countState',
default: 0
});
const countStrState = selector({
key: 'countStrState',
get: ({ get }) => {
const value = get(countState);
return '현재 값은 + value + '입니다.';
};
});
function Counter() {
/*함수 생략*/
const countStr = useRecoilValue(countStrState);
return (
<div>
<h1>Counter</h1>
<span id="value">{count}</span>
<div class="button-container">
<button class="btn-increase" onClick={increaseCount}>increase</button>
<button class="btn-decrease" onClick={decreaseCount}>decrease</button>
<button class="btn-reset" onClick={resetCount}>reset</button>
</div>
<div class="strValue">{countStr}</div>
</div>
);
}
selector는 atoms 값들에 의한 파생 값(상태)을 의미한다. 예를 들면 위와 같이 count를 받았다면 count를 포함하고 있는 문자열은 count로부터 파생된 값이라 말할 수 있다. selector 역시 useRecoilValue()로 가져올 수 있다.
selector의 get은 countStrState라고 countState 값만 가져올 수 있는 것이 아니라 다른 atom이 있다면 해당 값도 가져와서 사용할 수 있다. 예를 들어 atomA와 atomB가 있다면 selector에서 atomA와 atomB를 이용하여 새로운 파생 값을 만들어 사용할 수 있는 것이다.
selector에 속성으로 get이 있다면 당연히 set도 있을 것이다. selector의 set을 이용하여 문자열을 클릭하면 counter의 값을 100으로 바꾸는 기능을 구현해보자.
import React from 'react';
import { atom, useResetRecoilState, useRecoilValue, useSetRecoilState, selector } from 'recoil';
const countState = atom({
key: 'countState',
default: 0
});
const countStrState = selector({
key: 'countStrState',
get: ({ get }) => {
const value = get(countState);
return '현재 값은 + value + '입니다.';
},
set: ({ set }, _) => {
set(countState, 100);
}
});
function Counter() {
/*함수 생략*/
const setCountHundred = useSetRecoilState(countStrState);
return (
<div>
<h1>Counter</h1>
<span id="value">{count}</span>
<div class="button-container">
<button class="btn-increase" onClick={increaseCount}>increase</button>
<button class="btn-decrease" onClick={decreaseCount}>decrease</button>
<button class="btn-reset" onClick={resetCount}>reset</button>
</div>
<div class="strValue" onClick={setCountHundred}>{countStr}</div>
</div>
);
}
selector의 set 역시 useSetRecoilState()로 가져올 수 있다. 이를 div의 onClick속성에 연결하였다. set 내에서는 countState를 100으로 바꿔주도록 코드를 작성하였다.
selector의 set 역시 countStrState라고 countState 값만 바꿀 수 있는 것이 아니라 다른 atom이 있다면 해당 값도 가져와서 사용할 수 있다. 예를 들어 atomA와 atomB가 있다면 한 selector의 set에서 atomA와 atomB의 값 모두 변경할 수 있다.
Recoil을 써보면서 리액트 훅스를 사용한 입장에서는 배우기 쉬울 것이라는 생각이 많이 들었다. 또한 Redux에 비해 상태 값을 추가하거나 변경하기 위한 코드 변경이 확실히 적었다. 그리고 Redux는 비동기 처리를 위한 별도의 미들웨어가 필요했다. 그러나 Recoil은 useRecoilValueLoadable(), useRecoilStateLoable() 그리고 자체적인 캐싱을 지원하므로 이를 위한 별도의 코드를 작성할 필요가 없다.
Context API와 비교하면 어떨까? Context는 상태 값 마다 Context를 만들고 Provider를 제공해야하므로 여러 개의 상태를 내려주려면 Provider가 중첩되는 한계가 존재했다. 그렇다고 하나의 Context에 모든 상태 값을 몰아넣는다면 상태 값이 바뀔 때마다 재렌더링 되므로 렌더링 되지 않아도 되는 컴포넌트도 재렌더링되는 단점이 존재한다. Recoil은 Context의 이러한 단점에서 확실히 자유롭다.
코드: github.com/ImHyeLim1209/Recoil