Santos의 개발블로그

Recoil 본문

NPM/Valuable

Recoil

Santos 2023. 11. 4. 18:33

현재 회사에서 진행중인 프로젝트의 상태 관리는 Recoil이 담당하고 있다.

어떻게 하여 해당 라이브러리를 선택하게 되었는지 간략한 공유 후 Recoil 내부에 있는 함수에 대해서 공유드리는 시간을 가지려고 한다. 하나씩 살펴 보자!!

 


  

다른 상태 관리 라이브러리 였다면?

기존에 잘 알려져 있는 상태관리 라이브러리(Redux, Mobx 등)들은 리액트와 공생하고 있다. 상태를 관리하는 문제는 어떤 프로젝트를 하던 항상 대두되는 문제이고, 더 나은 해결책을 찾아나가는 과정속에서 여러 생각들을 하게 된다.

 

서드파트 라이브러리, 즉 프로그래밍을 도와주는 plug-in 이나 외부의 library들을 가져와 사용을 해야하는 react의 특성상 사이드이펙트는 항상 따라다니던 수식어였다. 상태관리 라이브러리도 마찬가지다. store가 외부요인으로 취급되어 React의 내부 스케줄러에 접근할 수 없기 때문에 동적으로 상태를 관리할 때 원하는 시점을 맞추기가 상당히 어려운 부분이었다.

 

특히 React에서 동시성 모드를 강조하며 상태 관리에서도 이를 손쉽게 사용할 수 있는 해결방안이 필요하였다.

💡 동시성 모드
1. 많은 작업을 한번에, 그들의 우선순위를 바꿔 가면서 동작하게 할 수 있게 하는 것
2. 큰 작업을 작은 여러개의 독립적인 작업으로 나누는 프로그래밍 구조로 싱글스레드의 한계를 뛰어넘어 우리의 앱을 더 효율적으로 만들어 주는 것

 

또한 비동기 데이터 처리 및 계산된 값에 대한 캐싱처리 등은 또 다른 라이브러리를 사용하여 해결해야 하고, 이는 더 복잡한 코드베이스를 가져가며, 가독성에도 좋지 않은 결과를 가져온다. react의 내부 스케줄러 시점을 정확하게 알지 못하니 memoization하는 것도 한계가 존재한다.

 

React에서 제공하는 ContextAPI는 어떨까? 낮은 빈도의 상태 값들을 업데이트한다면 나쁜 선택은 아니지만, 그렇지 않다면 상태관리 라이브러리를 사용하는 것이 효율적이다.

 

Provider 하위의 모든 consumer들은 Provider 속성이 변경이 될 때마다 다시 렌더링이 되기 때문에, 어플리케이션을 최적화하기에는 한계가 존재한다. 즉 업데이트가 많을 수록 더 많은 리렌더링을 컨트롤 해야 된다는 것이다.


왜 Recoil 이여야 할까?

React를 만든 Facebook에서 해당 라이브러리를 만들었다. 그렇기에 성능 및 효율성에서 더 나은 퍼포먼스를 보인다.

 

또한 hook을 베이스로 사용하기 때문에 많은 개발자들이 손 쉽게 배울 수 있는 장점이 있다.

 

마지막으로 데이터를 조각으로 나눌 수 있는 atom과 나눠진 데이터 조각을 계산할 수 있는 selector, 그리고 비동기 데이터 흐름을 제어할 수 있는 내장 솔루션까지 제공한다. 또한 React 동시성 모드까지 지원(Suspence 등) 하고 있어 더 개선된 생산성을 가져온다.


주요 기능

Atom

  • Recoil의 단위 데이터
  • atom 함수에 고유한 key, 기본값(defalut)를 작성 - defalut에는 정적인 값, Promise, RecoilValue가 들어올수 있음
  • 컴포넌트가 구독할 수 있는 React의 state라고 생각하면 편함
  • atom 값을 변경하면 구독하고 있는 컴포넌트들이 모두 다시 렌더링됨.
  • 비동기 데이터 흐름을 제어할 수 있음 with Promise
function atom<T>({
  key: string,
  default: T | Promise<T> | RecoilValue<T>,

  dangerouslyAllowMutability?: boolean,
}): RecoilState<T>

// 1. 정적인 값
export const nameState = atom({
  key: 'nameState',
  default: 'Jane Doe'
});

// 2. 비동기 제어(promise)
const counter = atom({
  key: 'counter',
  default: new Promise(resolve => {
    setTimeout(() => resolve(0), 10000);
  })
});

Seletor

  • 가공된 데이터를 받거나 가공하여 저장하고 싶을 때 사용 (동적인 데이터)
  • 순수 함수로서의 기능으로 사용
  • selector 함수에 고유한 key, get, set을 작성
  • 비동기 데이터 흐름을 제어할 수 있음 with Promise
  • get 메서드 내부에 있는 의존성의 종류와 값이 업데이트 될 때 구독하고 있는 모든 컴포넌트들이 리렌더링 됨
  • 캐시를 사용 - 입력값이 동일한 경우 get메서드를 다시 실행하지 않고 캐시에 있는 값을 반환 (key와 의존성 값으로 캐시키의 동일성을 확인)
// 1.계산된 값
const filteredAnimalsState = selector({
 key: 'animalListState',
 get: ({get}) => {
   const filter = get(animalFilterState);  ---------- atom
   const animals = get(animalsState); ---------- atom
   
   return animals.filter(animal => animal.type === filter); ---------- 계산된 값
 }
}); 

// 2. 비동기 제어(promise)
const recoilStar = selector({
  key: 'recoil/star',
  get: async () => {
    const response = await fetch(
      'https://api.github.com/repos/facebookexperimental/Recoil'
    );
    const recoilProjectInfo = await response.json();
    return recoilProjectInfo.stargazers_count;
  }
});

AtomFamily

  • Atom과 동일하지만, 다른 인스턴스와 구분이 가능한 매개변수를 받을 수 있음.
  • atomFamily는 내부적으로 memoization을 함. 그렇기에 각 인스턴스마다 고유한 키를 만들 필요가 없음.
// 1. atom (atom을 atomFamily처럼 표현했을때)
const itemWithId = memoize(id => atom({
  key: `item-${id}`,
  default: ...
}))

// 2. atomFamily without 매개변수
const itemWithId = atomFamily({
  key: 'item',
  default: ...
});

// 3. atomFamily with 매개변수
export const imageState = atomFamily({
  key: "imageState",
  default: id => getImage(id)
});

SeletorFamily

  • Selector와 동일하지만, 다른 인스턴스와 구분이 가능한 매개변수를 받을 수 있음.
  • get과 set 모두 파라미터를 전달받는 함수 생성기를 작성
  • 캐시를 사용 - 입력값이 동일한 경우 get메서드를 다시 실행하지 않고 캐시에 있는 값을 반환 (전달한 key 프로퍼티, get메소드 내부에 있는 의존성 종류 및 값, 전달된 파라미터 값으로 동일성을 확인)
  • 값의 동일성을 확인할 때 동등성을 확인 → 객체인 경우
// 1. selectorFamily
export const colorPickerSelectorFamily = selectorFamily<IColorPicker, string>({
  key: 'modalsSelectorFamily',

  get: (modalId) => ({get}) => get(colorPickerAtomFamily(modalId)), -------- 캐시 사용

  set: (modalId) => ({get, set, reset}, modalInfo) => {
    if (modalInfo instanceof DefaultValue) {
      reset(colorPickerAtomFamily(modalId));
      set(colorPickerIdsAtom, (prevValue) => prevValue.filter((item) => item !== modalId));

      return;
    }

    const modalIds = get(colorPickerIdsAtom); -------- 캐시 사용
    modalIds.forEach((id) => set(colorPickerAtomFamily(id), {id, isOpen: false}));

    set(colorPickerAtomFamily(modalId), modalInfo);
    set(colorPickerIdsAtom, (prev) => Array.from(new Set([...prev, modalInfo.id])));
  },
});

useHook 

useRecoilState

  • atom의 값을 구독하고 수정할 수 있는 hook인 useState와 동일한 방식으로 사용
const NameInput = () => {
  const [name, setName] = useRecoilState(nameState);
  const onChange = (event) => {
   setName(event.target.value);
  };
  return <>
   <input type="text" value={name} onChange={onChange} />
   <div>Name: {name}</div>
  </>;
}

 

useRecoilValue

  • setter 없이 atom의 값만 반환을 하는 방식으로 사용
const SomeOtherComponentWithName = () => {
  const name = useRecoilValue(nameState);
  return <div>{name}</div>;
}

 

useSetRecoilState

  • setter 함수만 반환
// useSetRecoilState  
const SomeOtherComponentThatSetsName = () => {
  const setName = useSetRecoilState(nameState);
  return <button onClick={() => setName('Jon Doe')}>Set Name</button>;
}

 

useResetRecoilState

  • defalut 값으로 초기화 할 수 있는 setter 함수만 반환

useRecoilSnapShot

  • snapShot를 가져오기 위한 hook
  • 상태가 변할 때마다 생성되기 때문에, 성능에 주의하며 사용
function SnapshotCount() {
  const snapshotList = useRef([]);
  const snapshot = useRecoilSnapshot();

  useEffect(() => {
    snapshotList.current = [...snapshotList.current, snapshot];
  }, [snapshot]);

  return (
    <p>Snapshot count: {snapshotList.current.length}</p>
  );
}

 

useRecoilCallback

  • useCallback 과 같이 의존성에 따라 갱신되는 함수를 생성.
  • snapShot 관련 기능을 전달하기 위해 한 번 더 감싸진 실행할 함수를 추가
  • selector 상태와 비동기 atom은 사용할 수 없음
  • return 값을 넘길 수 없음
const log1 = useCallback(() => {
  console.log('called with ', 'nothing');
});
const log2 = useRecoilCallback(({snapshot}) => () => {
  console.log('called with ', snapshot);
});

 

useRecoilTransaction

  • selector 상태와 비동기 atom은 사용할 수 없음
  • return 값을 넘길 수 없음
interface TransactionInterface {
  get: <T>(RecoilValue<T>) => T; // Recoil 상태를 가져온다
  set: <T>(RecoilState<T>,  (T => T) | T) => void; // 상태를 업데이트한다
  reset: <T>(RecoilState<T>) => void; // 상태를 초기화한다
}

// Args가 실제로 사용할 콜백의 파라미터들이 된다.
function useRecoilTransaction_UNSTABLE<Args>(
  callback: TransactionInterface => (...Args) => void,
): (...Args) => void

// 사용하기
const goForward = useRecoilTransaction_UNSTABLE(
  ({ get, set }) =>
    (distance) => {
      const heading = get(headingState);
      const position = get(positionState);

      set(positionState, {
        x: position.x + cos(heading) * distance,
        y: position.y + sin(heading) * distance,
      });
    },
);

 

'NPM > Valuable' 카테고리의 다른 글

[React-query] 데이터 filtering 하기  (0) 2024.03.02
Axios 뜯어보기  (2) 2023.11.30
cookie  (0) 2020.12.26
cookie-parser  (0) 2020.08.01
axios  (0) 2020.07.26
Comments