Recoil 라이브러리의 Atom, Selectors, Atom Effects에 대한 공부 기록이다.
Recoil 공식문서를 바탕으로 작은 프로젝트에 Reoil을 적용하는 과정을 기록할 예정이다.
Motivation
컴포넌트의 상태는 공통된 상위 요소까지 끌어올려야만 공유될 수 있으며,
이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 한다.
Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다.
공식 문서의 첫 번째 동기에서는 전역 상태 관리 기능을 사용하지 않았을 때 일어나는 문제점에 대해서 이야기한다.
두 번째 동기에서는 Context를 사용했을 때 발생하는 한계점에 대해 이야기하는데,
이는 이전 기록에서 다뤘던 문제점이다.
Context는 두 개 이상의 global state를 사용하기 위해서는
각 global state 마다 Context 객체를 만들어 따로 Provider를 통해 제공해줘야 하는 번거로움이 있었다.
이제 Recoil이 어떤 방식으로 위의 문제를 해결하고, 추가로 어떤 기능을 제공하는지 보자.
Installation
먼저 recoil 라이브러리를 설치해보자.
$npm install recoil
RecoilRoot
Context를 사용할 때와 마찬가지로 global state를 사용할 컴포넌트의 상위 컴포넌트
즉, Provider의 기능을 Recoil에서는 RecoilRoot를 통해 제공한다.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { RecoilRoot } from "recoil";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<>
<RecoilRoot>
<App />
</RecoilRoot>
</>
);
위와 같이 App 컴포넌트를 RecoilRoot 컴포넌트로 감싸줌으로써
모든 컴포넌트에서 global state를 사용할 수 있도록 해준다.
이제 global state를 생성해보자..
Atom
Recoil은 Atom을 통해 global state를 생성할 수 있게 해준다.
Atom은 atom() 메서드를 이용해 생성한다.
생성 예시는 아래와 같다.
export const Category = atom({
key: "Category",
default: "TO_DO",
});
- key : atom마다 가지는 고유한 key값이다.
- default : atom 값이 지정되지 않았을 때 가질 초기값이다.
생성한 global state를 사용하는 방법은 아래와 같다.
useRecoilState, useRecoilValue, useSetRecoilState
atom을 사용하는 데에는 3가지 방법이 있다.
첫 번째 useRecoilState는 useState와 유사한 방법으로 atom을 사용한다.
import { useRecoilState } from "recoil";
import { Category } from "../atoms/atoms";
const [category, setCategory] = useRecoilState(Category);
생성한 Atom을 가져오고,
useRecoilState의 인자로 전달한다.
반환 값은 useState와 동일하게 atom의 현재 값, atom에 대한 수정 함수이다.
useRecoilValue
import { useRecoilValue } from "recoil";
import { Category } from "../atoms/atoms";
const category = useRecoilValue(Category);
useRecoilValue는 위에서 atom의 현재 값만 반환하는 메서드이다.
useSetRecoilState
import { useSetRecoilState } from "recoil";
import { Categories } from "../atoms/atoms";
const setCategory = useSetRecoilState(Categories);
useSetRecoilState는 atom의 수정 함수만 반환하는 메서드이다.
이처럼 Recoil 라이브러리를 사용하면,
여러 global state에 대해 atom을 만들긴 하지만 RecoilRoot 하나의 컴포넌트만으로도
생성한 global state를 모두 useState와 유사한 익숙한 방법으로 사용할 수 있다는 장점이 있다.
Selector
selector는 global state의 일부를 반환한다.
상태를 기반하는 파생 데이터를 계산하는 데 사용되므로
공식 문서에서는 최소한의 상태 집합만 atom에 저장하고
다른 모든 파생되는 데이터는 selectors 에 명시한 함수를 통해 효율적으로 계산함으로써
쓸데없는 상태의 보존을 방지할 것을 권장하고 있다.
작은 프로젝트인 카테고리 Todo List에 selector을 사용해보자.
export const TodoSelector = selector({
key: "todoSelector",
get: ({ get }) => {
const toDos = get(TodoArr);
const category = get(selectedCategory);
return toDos.filter((todo) => todo.category === category);
},
});
- key : selector가 갖는 고유한 key 값이다.
- get : get 속성에서는 함수를 값으로 가진다.
- get() : 옵션 중 get 메서드를 사용할 수 있는데 get 메소드를 통해 atom의 값을 가져올 수 있다.
위와 같이 todo 리스트와 선택된 카테고리를 값으로 가져오고,
filter 메소드를 이용해 원하는 카테고리의 todo 리스트만 반환해줄 수 있다.
공식문서에서 다른 좋은 예시를 보자.
const todoListStatsState = selector({
key: 'todoListStatsState',
get: ({get}) => {
const todoList = get(todoListState);
const totalNum = todoList.length;
const totalCompletedNum = todoList.filter((item) => item.isComplete).length;
const totalUncompletedNum = totalNum - totalCompletedNum;
const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum;
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
};
},
});
공식문서의 예제를 가져왔다.
위처럼 todoList라는 상태에 대해 파생되는 값들을 한 번에 반환해줄 수 있어서
상태에 관련된 로직 분리와 효율적인 렌더링을 할 수 있다.
Effects
effects 속성에서는 atom을 초기화 또는 동기화하기 위한 API이다.
이번 기록에서는 위의 todo list를 새로고침 해도 없어지지 않도록 local storage에 보관하는 작업만 다뤄보기로 하자.
Atom Effect의 구성을 보자.
type AtomEffect<T> = ({
node: RecoilState<T>, // A reference to the atom itself
storeID: StoreID, // ID for the <RecoilRoot> or Snapshot store associated with this effect.
trigger: 'get' | 'set', // The action which triggered initialization of the atom
// Callbacks to set or reset the value of the atom.
// This can be called from the atom effect function directly to initialize the
// initial value of the atom, or asynchronously called later to change it.
setSelf: (
| T
| DefaultValue
| Promise<T | DefaultValue> // Only allowed for initialization at this time
| ((T | DefaultValue) => T | DefaultValue),
) => void,
resetSelf: () => void,
// Subscribe to changes in the atom value.
// The callback is not called due to changes from this effect's own setSelf().
onSet: (
(newValue: T, oldValue: T | DefaultValue, isReset: boolean) => void,
) => void,
// Callbacks to read other atoms/selectors
getPromise: <S>(RecoilValue<S>) => Promise<S>,
getLoadable: <S>(RecoilValue<S>) => Loadable<S>,
getInfo_UNSTABLE: <S>(RecoilValue<S>) => RecoilValueInfo<S>,
}) => void | () => void; // Optionally return a cleanup handler
나는 setSelf와 onSet만 사용하려고 한다.
- setSelf : setSelf는 atom 값을 설정하는 메서드이다. 새로고침 하고 난 이후 local storage의 값을 가져와서 atom의 값으로 설정해주는 역할을 할 것이다.
- onSet : atom의 값이 변할 때마다 작동하는 메서드이다. 변경된 atom 값을 local storage에 넣는 역할을 할 것이다.
해야 할 일을 정리해보면,
새로고침을 하기 이전에 atom 값이 변경할 때마다 local storage에 atom 값을 저장,
새로고침을 한 이후 local storage에 저장된 값을 atom 값으로 설정
import { atom, selector } from "recoil";
interface ITODO {
text: string;
id: number;
category: string;
}
const dataEffect = {
//local storage에 저장할 때 사용할 key 값 지정
(key: string) =>
//인자로 onSet, setSelf를 가져옴
({ onSet, setSelf }: any) => {
//local storage에 저장된 값을 가져옴
const data = localStorage.getItem(key);
if (data !== null) {
//data가 null이 아닐 때 setSelf 메소드를 이용해 atom 값으로 설정해줌
setSelf(JSON.parse(data));
}
//atom의 값이 변경될 때마다 local storage에 저장
onSet((newValue: any) => {
localStorage.setItem(key, JSON.stringify(newValue));
});
};
//새로 만든 category와 Todo List의 값을 effects 속성을 이용해 저장
export const Categories = atom({
key: "Categories",
default: ["normal"],
effects: [dataEffect("Categories")],
});
export const TodoArr = atom<ITODO[]>({
key: "todoArr",
default: [],
effects: [dataEffect("TodoList")],
});
}
effects는 다음과 같이 배열을 값으로 가지는데,
생성한 함수를 배열의 원소로 전달하면 된다.
참고
마치며
잘못된 정보에 대한 피드백은 환영입니다.
감사합니다.
'Frontend > React' 카테고리의 다른 글
[ReactJS] React 스터디(1)(node, VirtualDOM, CRA, npm&yarn, 컴포넌트) (0) | 2023.01.09 |
---|---|
[React] 폰트 깜빡임 현상 해결(Global Style, CSS) (0) | 2022.12.29 |
[React] Context, useContext, Recoil(전역 상태 관리) (0) | 2022.11.30 |
[React] CRA 프로젝트 초기 설정(ESLint,Prettier) (0) | 2022.11.16 |
[React] Server State&Client State,React-Query (0) | 2022.09.30 |