본문 바로가기

Frontend/React

[React] Recoil(Atom, Selectors, Atom Effects)

728x90
Recoil 라이브러리의 Atom, Selectors, Atom Effects에 대한 공부 기록이다.
Recoil 공식문서를 바탕으로 작은 프로젝트에 Reoil을 적용하는 과정을 기록할 예정이다.

Motivation

컴포넌트의 상태는 공통된 상위 요소까지 끌어올려야만 공유될 수 있으며,
이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 한다.

Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다.
 

동기 | Recoil

호환성 및 단순함을 이유로 외부의 글로벌 상태관리 라이브러리보다는 React 자체에 내장된 상태 관리 기능을 사용하는 것이 가장 좋다.

recoiljs.org

공식 문서의 첫 번째 동기에서는 전역 상태 관리 기능을 사용하지 않았을 때 일어나는 문제점에 대해서 이야기한다.

 

두 번째 동기에서는 Context를 사용했을 때 발생하는 한계점에 대해 이야기하는데,

이는 이전 기록에서 다뤘던 문제점이다.

 

[React] Context, useContext, Recoil(전역 상태 관리)

전역 상태 관리에 대한 공부를 하던 중 React에서 지원하는 Context와 Recoil의 사용법과 차이에 대한 정리를 했다. 이에 대한 기록이다. Context API context를 사용할 때 고려해야 할 점 context의 주된 용도

choi-records.tistory.com

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의 값을 가져올 수 있다.

get 메소드의 call signature다. recoil value를 반환하는 것을 볼 수 있다.

위와 같이 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는 다음과 같이 배열을 값으로 가지는데,

생성한 함수를 배열의 원소로 전달하면 된다.


참고

 

주요 개념 | Recoil

개요

recoiljs.org

 

 

Recoil with Storage (feat. effects) - 오픈소스컨설팅 테크블로그

오픈소스컨설팅 테크블로그 Recoil with Storage (feat. effects) - Web Frontend Library React.js 의 상태관리 라이브러리인 Recoil 에 대해서 심층적으로 탐구해봅니다.

tech.osci.kr

마치며

잘못된 정보에 대한 피드백은 환영입니다.

감사합니다.

728x90