React에서 전역상태를 관리하기 위한 여러 라이브러리 중 하나이며, 기존 리액트 개발자라면 쉽게 사용이 가능하다.
공식문서에는 Recoil을 사용하면 atoms (공유 상태)에서 selectors (순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다. Atoms는 컴포넌트가 구독할 수 있는 상태의 단위다. Selectors는 atoms 상태 값을 동기 또는 비동기 방식을 통해 변환한다.
리액트에서 자식컴포넌트에서 부모(상위)컴포넌트의 state를 수정이 필요하면 부모(상위)컴포넌트에서 setState함수나, state를 변경하는 함수를 자식 컴포넌트에게 넘겨줘야 한다. 간단한 프로젝트 일경우는 상관이 없으나, 부모 자식관계의 깊이가 커질수록 위에 방식처럼 부모에서 자식으로 props로 넘겨줘야 한다.(prop drilling) 이럴 경우에 전역상태 라이브러리를 사용하면 된다.(Redux, Recoil, Mobx, Zustand...) Recoil은 Redux에 비해 간단하며, 비교적 보일러 플레이트 코드가 적다. 리액트를 사용한 개발자라면 쉽게 익힐수 있고, 러닝커브가 적다.
atom은 쉽게 생각하면 하나의 상태라고 알고 있으면 된다. atom이 업데이트 되면, 해당 atom을 구독하고(사용) 있던 컴포넌트들이 리랜더링이 일어난다.
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {RecoilRoot} from "recoil";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>
);
reportWebVitals();
// src > recoil > atoms > input.ts
import {atom} from "recoil";
export const inputState = atom({
key: 'inputState',
default: ''
})
// App.tsx
import React from 'react';
import {useRecoilState} from "recoil";
import {inputState} from "./input";
function App() {
const [input, setInput] = useRecoilState(inputState);
return (
<div>
<input value={input} onChange={(e) => {
setInput(e.target.value)
}} />
</div>
);
}
export default App;
우선 atom을 사용하기 위해서는 먼저 RecoilRoot 컴포넌트를 사용할려는 컴포넌트 상위에 부모컴포넌트로 덮어줘야 한다. 그다음은 atom에 key와 default 값을 설정해주면 된다. key는 다른 atom과 겹치지 않는 고유한 값으로 지정해주면 되고 default 값은 초기값을 지정해주면 된다.
그리고 atom state를 사용할려면 사용할려는 컴포넌트에서 useRecoilState를 사용해서 [state, setState] 방식으로 값을 가져오면 된다. 배열의 첫번째값은 value이고 두번째값은 setState할려는 setter 함수이다. 리액트 setState와 유사하다.(이름도 비슷하고 사용방법은 같다)
// react에서 state를 사용할때랑 비슷(?)
const [input,setInput] = useState('');
혹은 값만 가져오고 싶거나(useRecoilValue) setState 함수(useSetRecoilState)만 가져오고 싶을 경우에도 recoil에서 hooks를 제공하고 있다.
// const [input, setInput] = useRecoilState(inputState);
const input = useRecoilValue(inputState);
const setInput = useSetRecoilState(inputState);
...
간단한 프로젝트에서는 atom만 사용하더라고 큰 문제는 없다.
Selector는 파생된 상태(derived state)의 일부를 나타낸다. 파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.
위에 글만 봐서는 헷갈리수 있는데 간단하게 말하면, Selector는 Recoil 내에서 상태(state) 값을 변환하거나 계산하기 위해 사용할때 사용하고, Recoil 상태의 파생된 값을 만들기 위한 함수라고 생각하면 되고 입력값이 같으면 항상 출력은 동일해야 하는 순수함수 이다.
이전에 정의한 하나 이상의 atom 값을 기반으로 하여 새로운 값을 계산하거나 변환할 수 있다.
selector 내부에서 사용되는 키값에 대해서 알아보자.
selector({
key: 'squaredSelector',
get: ({ get }) => {
const inputNumber = get(inputNumberState);
return inputNumber * inputNumber;
},
get: ({ get }) => {
const inputNumber = get(inputNumberState);
return inputNumber * inputNumber;
},
set: ({ set }, newValue) => {
const newInputNumber = Math.sqrt(newValue);
set(inputNumberState, newInputNumber);
},
})
key: atom의 키값과 동일하게 유일한 값이여야 한다.
get: derived state(파생 상태) 를 return 하는 메소드
set: writeable 한 state(atom) 값을 변경할 수 있는 메소드
"get" 함수 내부에서는 해당 Selector가 의존하는 atom 또는 다른 Selector의 값이 변경될 때마다 "get" 함수가 다시 호출된다.
selector 내부에서 비동기 통신 위한 예제
export const fetchPostsFromServer = async () => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
return response.data;
} catch (error) {
throw error; // 에러 처리
}
};
export const getPostsSelector = selector<any>({
key: 'getPosts',
get: async () => {
try {
const posts = await fetchPostsFromServer();
return posts;
} catch (error) {
console.error('Error fetching posts:', error);
throw error;
}
},
});
// PostLists.jsx
.
.
.
const posts = useRecoilValue(getPostsSelector);
.
.
return (
<>
{posts.map((list) => (
<li key={list.title}>{list.title}</li>
))}
</>
);
useRecoilValue를 사용해서 selector값을 가져오면 된다. 이렇게만 사용하면 브라우저 콘솔에 warning이 뜨는데 Suspense를 이용해서 해당 컴포넌트를 감싸면 이에러는 사라진다.
- Suspense 컴포넌트
Suspense 컴포넌트 역할 중 하나는 서버로부터 비동기 데이터나 리소스를 가져올 때 로딩 중일 때의 UI 상태를 관리한다. 이외에도 코드스플릿팅도 지원한다(해당 컴포넌트가 비동기로 로딩될때 fallbackUI로 설정한 컴포넌트가 보여진다.)
import React, { Suspense } from 'react';
.
.
.
<Suspense fallback={<div>loading...</div>}>
<PostList />
</Suspense>
또다른 방법으로는 Suspense 컴포넌트를 사용하지않고, recoil에서 제공하는 useRecoilValueLoadable hook을 사용하면 된다.
import { useRecoilValueLoadable } from 'recoil';
const posts = useRecoilValueLoadable(getPostsSelector);
switch (posts.state) {
case 'loading':
return <p>loading...</p>;
case 'hasError':
return <p>Error: {posts.contents.message}</p>;
default:
const postsData = posts.contents;
return postsData.map((list) => <div key={list.title}>{list.title}</div>);
}
아래 코드에서 posts의 값은 객체인데 { state: , contents: } 이런식으로 이루어져 있다. state의 값인'loading' || 'hasError' || 'hasValue'
이 셋중 하나로 로딩 상태이거나 값이 있는 상태(정상적으로 비동기 통신 완료) 이거나 error 상태로 나누어져 있다.
state값이 'hasValue' 가 되면 contents의 값(서버에서 받아온 값)을 통해서 보여주면 되는 방식이다.
selector를 사용하면 내부적으로 내부적으로 캐시를 해준다는 장점이 있지만, 이것이 오히려 독이 되서 이전(오래된) 데이터를 보여줄때도 있다. (구독하고 있는 state가 있다면 해당 state가 변경이 되면 호출되는 방식이다)
const refresh = useRecoilRefresher_UNSTABLE(getPostsSelector);
.
.
.
<button onClick={refresh}>초기화</button>
useRecoilRefresher_UNSTABLE(selector변수)를 이용해서 캐시를 초기화 하는 방법도 있다.
파라미터 값을 selector에서 사용할 경우에 selectorFamily를 사용하면 된다.
export const postSelector = selectorFamily({
key: 'postSelector',
get: (postId: number) => async ({ get }) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
const data = await response.json();
return data;
},
});
// Post.jsx
const { postId } = useParams();
const post = useRecoilValueLoadable(postSelector(Number(postId)));
좋은 정보 얻어갑니다, 감사합니다.