리코일은 상태 관리를 위한 페이스북(현 메타)의 라이브러리로, React 최신 기능과 호환되면서도 React 만으로는 달성하기 어려운 여러 기능을 제공한다.
먼저 어떠한 라이브러리든 솔루션이든 접하게 되었을 때 가장 먼저 드는 생각은 '왜 만들어졌고, 무엇을 해결하기 위해 등장하였는지'에 관해서이다.
이에 대해 개발 의도를 제작자의 프레젠테이션이 담긴 아래 영상을 보니 이해하는 데 도움이 되었다.
(링크: Recoil: State Management for Today's React - Dave McCabe)
먼저 이들은 기존의 상태 관리를 위한 솔루션만으로는 한계점을 느꼈다고 한다. 다음은 언급되었던 문제 중 일부이다.
그리하여 트리의 말단(실제 상태가 사용되는 곳)에서 트리의 상단(실제 상태가 존재하는 곳)과 결합하지 않고도, 상태를 필요로 하는 트리의 말단끼리 서로 협력할 수 있도록 하는 아이디어로부터 리코일이 등장하였다.
또한 React적인 방식으로 앞선 문제들을 개선해 나가고자 아래와 같은 핵심 접근 방식을 따르고 있다.
아직까지도 Git 저장소 설명에는 '리액트를 위한 실험적인 상태 관리 라이브러리'라는 설명으로 안내되고 있어 리코일을 프로덕션에 적용하거나 사용해보기 꺼려진다는 의견들도 존재했다.
이와 관련한 답변에 따르면, Meta팀은 모든 React 기능과 호환되는 솔루션이라는 확신이 들기까지는 프로젝트를 실험적인 상태로 유지할 것이고, 이미 일부 프로덕션 환경에는 리코일이 적용되어 있는 프로젝트가 있다고도 답변하였다.
이 부분에 관해서는 라이브러리에 대한 좀 더 많은 경험치와 숙련도가 쌓여야 판단을 내릴 수 있을 것 같다!
리코일이 무엇을 하는 솔루션이며 어떻게 등장하게 되었는지에 대한 배경도 알아보았으니 실제로 사용해보기 위한 기초적인 지식에 대해 학습해보았다.
이 글에서는 리코일에서 사용되는 핵심적인 구성 요소들에 대한 소개와 이들이 적용된 간단한 코드에 대한 설명을 정리하였다.
비동기 데이터 쿼리나 트랜지션 등 리코일만의 몇몇 강력한 기능들은 추후 별도로 정리해보고자 한다.
① Atoms
key
값을 가져야 한다.// Atom 정의
atom({
key: 'fontSizeState',
default: 14,
});
// Atom 읽고 쓰기
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
...
setFontSize((size) => size + 1)
② Selectors
파생된 상태 (Derived state)
주어진 종속성 값 집합에 대해 항상 동일한 값을 반환하며 side-effect가 없는 "멱등성" 또는 "순수 함수"와 유사하다고 생각하면 된다.
get
인자를 통해 다른 Atom/Selector를 참조한다. 참조되는 모든 Atom/Selector가 종속성 목록에 추가되어 해당 Atom/Selector 업데이트 시 이 함수도 재실행된다.// Selector 정의
selector({
key: 'fontSizeLabelState,
get: ({ get }) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
// Selector 읽기
const fontSizeLabel = useRecoilValue(fontSizeLabelState);
...
return <div>Current font size: ${fontSizeLabel}</div>;
☆ 최소한의 상태 집합만 Atoms에 저장하고,
☆ 그외 파생되는 데이터는 Selectors 함수를 통해 효율적으로 계산할 수 있도록 한다.
useRecoilState()
: atom을 읽고 쓸 때 사용useRecoilValue()
: atom을 읽기만 할 때 사용 useSetRecoilState()
: atom에 쓰려고 할 때 사용useResetRecoilState()
: atom을 기본값으로 초기화할 때 사용참고로 컴포넌트에서 useRecoilState
와 useRecoilValue
같은 훅을 사용하면, 상태 업데이트 시 컴포넌트가 다시 렌더링될 수 있도록 구독을 수행한다.
리코일 공식 문서에서 대화형 튜토리얼 샘플 코드가 제공되어 해당 튜토리얼을 따라서 해 보았다.
https://app.sideguide.dev/recoil/tutorial/ 에서 진행할 수 있다.
예제에서는 간단한 할 일 목록을 나타내는 Todo List를 만들고, 할 일을 추가/수정/삭제하고 필터링하는 기능 및 유용한 통계를 표시하는 기능을 구현하고 있다.
RecoilRoot
리코일이 동작하도록 하려면 컴포넌트 트리의 상위에 RecoilRoot
를 배치시켜야 한다.
적절한 위치로는 루트 컴포넌트(<App />
컴포넌트) 가 있다. 아래와 같이 루트 컴포넌트를 RecoilRoot
로 감싸준다.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>
);
TodoList
Atom 생성Atom은 상태를 나타내는 원천 데이터와도 같다. Todo List에서 Atom은 할 일을 나타내는 항목의 배열이 될 것이다. 다음과 같이 생성할 수 있다.
const todoListState = atom({
key: 'TodoList', // 식별을 위한 고유한 키 값
default: [], // 기본 값 (빈 배열)
});
참고로 단일 Todo 항목 객체는 다음과 같은 구조를 갖는다.
{
id: number, // 식별을 위한 고유한 키 값
text: string, // 할 일 내용
isComplete: boolean, // 완료 여부
}
TodoListFilter
Atom 생성TodoList
Atom 외에도 필터링 기준이 되는 값을 저장하기 위해 TodoListFilter
Atom을 추가 생성한다.
필터 옵션이 될 수 있는 값으로는 "Show All", "Show Completed", "Show Uncompleted"가 있을 수 있다. 기본 값은 "Show All"(전체 보기)이다.
const todoListFilterState = atom({
key: 'TodoListFilter',
default: 'Show All',
});
TodoList
컴포넌트해당 컴포넌트는 필터링된 할 일 목록을 렌더링한다. 기존 할 일 목록을 기반으로 파생될 값이기 때문에 Selector로 정의하였다. 여기서는 내부적으로 2가지 종속성(todoListFilterState
, todoListState
)을 추적하고 있어 둘 중 하나라도 변경되면 최신 값이 반영될 수 있도록 재 렌더링된다.
그리고 나서 리코일 상태의 값을 읽기 위해 useRecoilValue()
훅을 사용했다. 전달되는 인자가 todoListState
가 아닌 filteredTodoListState
이기 때문에 필터링된 목록을 보여줄 수 있게 된다.
const filteredTodoListState = selector({
key: "filteredTodoListState",
get: ({ get }) => {
const filter = get(todoListFilterState);
const list = get(todoListState);
switch (filter) {
case "Show Completed":
return list.filter((item) => item.isComplete);
case "Show Uncompleted":
return list.filter((item) => !item.isComplete);
default: // "Show All"
return list;
}
}
});
...
function TodoList() {
const todoList = useRecoilValue(filteredTodoListState);
return (
<>
...
{todoList.map((todoItem) => (
<TodoItem item={todoItem} key={todoItem.id} />
))}
</>
);
}
TodoListFilter
컴포넌트필터링 기준이 되는 값을 변경할 수 있는 컴포넌트이다. 총 3가지 옵션 값이 있고 이들 사이에서 상태가 전환될 수 있도록 한다.
function TodoListFilters() {
const [filter, setFilter] = useRecoilState(todoListFilterState);
const updateFilter = ({ target: { value } }) => {
setFilter(value);
};
return (
<>
Filter:
<select value={filter} onChange={updateFilter}>
<option value="Show All">All</option>
<option value="Show Completed">Completed</option>
<option value="Show Uncompleted">Uncompleted</option>
</select>
<p></p>
</>
);
}
TodoItem
컴포넌트해당 컴포넌트는 단일 Todo 항목을 렌더링한다. 여기서는 할 일 항목을 표시하고, 그 밖에도 텍스트를 변경하거나 삭제할 수 있다. 이는 useRecoilState()
훅을 통해서 이루어진다. 아래와 같이 todoListState
를 읽거나 쓸 수 있게 만들 수 있다.
const [todoList, setTodoList] = useRecoilState(todoListState);
TodoItemCreator
컴포넌트해당 컴포넌트에서는 새로운 Todo 항목을 생성할 수 있도록 한다. 이를 위해 todoListState
를 업데이트할 수 있는 setter 함수가 필요한데, useSetRecoilState()
훅을 통해 달성 가능하다.
const setTodoList = useSetRecoilState(todoListState);
const addItem = () => {
setTodoList((oldTodoList) => [
...oldTodoList,
{ id: getId(), text: inputValue, isComplete: false },
]);
setInputValue("");
};
"할 일 목록 통계"는 "할 일 목록 필터링" 구현과 동일한 개념으로 상세한 설명은 생략하였다.
전역 상태를 간단하고 효과적으로 관리할 수 있도록 도와주는 것 같다. Atom과 Selector라는 2가지 개념을 통해 상태를 정의하고, 읽고, 쓸 수 있는 방식이 직관적이었다. 기존 React의 훅인 useState
를 사용하는 것과 유사하여 러닝 커브 또한 그렇게 어렵지 않았던 것 같다.
컴포넌트에서 리코일 훅을 사용하기만 해도 별 다른 설정 없이도 구독이 이루어져, 상태가 변경되면 자동으로 그 부분만 Re-rendering이 일어나는 점이 좋았다.
컴포넌트 관점에서는 Atom이나 Selector나 동일한 인터페이스를 가지고 있어 서로 대체할 수 있는 개념이라고는 하지만, 실제 프로젝트에서는 어떠한 값을 Atom으로 정의할지 어떠한 값을 Selector로 정의할지 그 특성에 따라 컴포넌트 설계 방식에 대한 고민이 필요할 것 같다.
추후 비동기 데이터 쿼리라든가 DevTools 이라든가 리코일만의 다양한 기능들도 모두 학습하고 적용시켜보고자 하는 욕구가 샘솟는다!
🆚 Redux
🆚 Context API