
Recoil은 Facebook에서 개발한 React를 위한 상태 관리 라이브러리이다. React의 Concurrent Renderer를 공식적으로 지원하는 유일한 상태 관리 라이브러리이며, React의 컨셉과 유사하게 설계되어 있어 사용법이 직관적이고 러닝 커브가 낮다는 장점이 있다.
Recoil이란?
Recoil은 React에서 전역상태를 관리하기 위한 여러 라이브러리 중 하나이다. 기존 리액트 개발자라면 쉽게 사용이 가능하며, 공식문서에 따르면 Recoil을 사용하면 atoms(공유 상태)에서 selectors(순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다.
참고: Recoil 저장소는 2025-01-01부로 GitHub에서 아카이브 처리되어 더 이상 신규 기능이 추가되지 않는다고 한다. 안정적으로 동작은 하지만 장기 유지보수 측면에서 Zustand·Jotai·TanStack Store 등 대안도 있으니 검토할 필요가 있다.
리액트에서 자식컴포넌트에서 부모(상위)컴포넌트의 state를 수정이 필요하면 부모(상위)컴포넌트에서 setState함수나, state를 변경하는 함수를 자식 컴포넌트에게 넘겨줘야 한다. 간단한 프로젝트의 경우는 상관이 없으나, 부모 자식관계의 깊이가 커질수록 위에 방식처럼 부모에서 자식으로 props로 넘겨줘야 한다(prop drilling). 이럴 경우에 전역상태 라이브러리를 사용하면 된다(Redux, Recoil, Mobx, Zustand...). Recoil은 Redux에 비해 간단하며, 비교적 보일러 플레이트 코드가 적다. 리액트를 사용한 개발자라면 쉽게 익힐수 있고, 러닝커브가 낮다.
Atom
Atom은 Recoil의 기본 상태 단위이다. 컴포넌트에서 구독할 수 있는 공유 상태를 의미한다. Atom이 업데이트되면 해당 atom을 구독하고 있는 컴포넌트들이 리렌더링된다.
import { atom } from 'recoil';
export const countState = atom({
key: 'countState', // 고유한 키
default: 0, // 초기값
});
Atom을 사용하는 방법은 다음과 같다.
import { useRecoilState } from 'recoil';
import { countState } from './atoms';
function Counter() {
const [count, setCount] = useRecoilState(countState);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Atom의 상태를 읽기만 하거나 설정만 하고 싶을 때는 다음과 같은 훅을 사용할 수 있다.
// 값만 읽기
const count = useRecoilValue(countState);
// 값만 설정하기
const setCount = useSetRecoilState(countState);
Selector
Selector는 다른 atom이나 selector의 값을 기반으로 계산된 파생 상태이다. Selector는 순수 함수로, 입력값이 같으면 항상 동일한 출력을 반환해야 한다.
import { selector } from 'recoil';
import { countState } from './atoms';
export const doubleCountState = selector({
key: 'doubleCountState',
get: ({get}) => {
const count = get(countState);
return count * 2;
},
});
Selector는 읽기 전용이지만, set 속성을 추가하여 쓰기 기능을 구현할 수 있다.
export const countState = selector({
key: 'countState',
get: ({get}) => {
const count = get(countState);
return count;
},
set: ({set}, newValue) => {
set(countState, newValue);
}
});
SelectorFamily
SelectorFamily는 파라미터를 받아 동적으로 selector를 생성할 수 있게 해주는 기능이다. 특히 API 호출 시 동적 파라미터가 필요한 경우 유용하다.
import { selectorFamily } from 'recoil';
export const userState = selectorFamily({
key: 'userState',
get: (userId) => async () => {
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
return user;
}
});
기본적인 비동기 처리
Recoil의 selector를 사용하여 비동기 데이터를 처리할 수 있다.
import { selector } from 'recoil';
export const userState = selector({
key: 'userState',
get: async () => {
const response = await fetch('https://api.example.com/user');
const user = await response.json();
return user;
},
});
Suspense를 활용한 로딩 처리
비동기 데이터를 사용할 때는 Suspense를 사용하여 로딩 상태를 처리할 수 있다.
import { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { userState } from './selectors';
function UserProfile() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserInfo />
</Suspense>
);
}
function UserInfo() {
const user = useRecoilValue(userState);
return <div>{user.name}</div>;
}
useRecoilValueLoadable
Suspense를 사용하지 않고 로딩 상태를 처리하고 싶다면 useRecoilValueLoadable을 사용할 수 있다.
import { useRecoilValueLoadable } from 'recoil';
function UserInfo() {
const userLoadable = useRecoilValueLoadable(userState);
switch (userLoadable.state) {
case 'hasValue':
return <div>{userLoadable.contents.name}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
return <div>Error: {userLoadable.contents.message}</div>;
}
}
Atom Effects
Atom Effects는 atom의 생명주기 동안 특정 동작을 수행할 수 있게 해주는 기능이다. 예를 들어, localStorage와 연동하여 상태를 유지할 수 있다.
const persistAtom = atom({
key: 'persistAtom',
default: null,
effects: [
({setSelf, onSet}) => {
const savedValue = localStorage.getItem('persistAtom');
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
onSet((newValue) => {
localStorage.setItem('persistAtom', JSON.stringify(newValue));
});
},
],
});
Recoil Snapshot
Recoil Snapshot을 사용하면 상태의 스냅샷을 찍고, 이를 통해 상태를 시간에 따라 추적하거나 디버깅할 수 있다.
import { useRecoilSnapshot } from 'recoil';
function DebugObserver() {
const snapshot = useRecoilSnapshot();
useEffect(() => {
console.log('The following atoms were modified:');
for (const node of snapshot.getNodes_UNSTABLE({isModified: true})) {
console.log(node.key, snapshot.getLoadable(node).contents);
}
}, [snapshot]);
return null;
}
Recoil의 장점, 한계점
Recoil의 장점을 정리해 보았다.
- 간단한 API: React의 useState와 유사한 API를 제공하여 쉽게 학습하고 사용할 수 있다.
- 성능 최적화: 상태 변경 시 변경된 부분만 렌더링하여 성능을 최적화한다.
- React 친화적: React의 Hook과 유사한 API를 제공하여 React 개발자에게 친숙한 사용 경험을 제공한다.
- 비동기 처리 용이: selector를 통해 비동기 데이터 처리를 쉽게 구현할 수 있다.
- Concurrent Mode 지원: React의 Concurrent Mode를 공식적으로 지원한다.
- 캐싱 기능: selector를 통한 비동기 데이터 처리는 자동으로 캐싱되어 성능을 최적화한다.
- TypeScript 지원: TypeScript와의 통합이 잘 되어 있어 타입 안정성을 보장한다.
하지만 Recoil은 아래와 같은 단점도 존재한다.
- 업데이트 중단: 위에서 다루었던 내용으로, 더 이상 새로운 기능이나 버그 수정이 이루어지지 않는다.
- DevTools 부재: Redux DevTools와 같은 강력한 디버깅 도구가 아직 없다.
- 상태 관리의 복잡성: 대규모 애플리케이션에서는 상태 관리가 복잡해질 수 있다.
- 커뮤니티 크기: Redux에 비해 상대적으로 작은 커뮤니티를 가지고 있다.