[React] Recoil

채동기·2022년 11월 28일
0

react

목록 보기
4/5

만들어진 이유

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

  • 컴포넌트의 상태는 공통된 상위요소까지 끌어올려야만 공유될 수 있으며, 이 과정에서 거대한 트 리가 다시 렌더링되는 효과를 야기하기도 한다.
  • Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다.
  • 이 두가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 말단(state가 사용되는 곳)까지의 코드 분할을 어렵게한다.

개요

Recoil을 사용하면 atoms (공유 상태)에서 selectors (순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다. Atoms는 컴포넌트가 구독할 수 있는 상태의 단위다. Selectors는 atoms 상태값을 동기 또는 비동기 방식을 통해 변환한다.

Atoms

간단히 이해하고자 한다면 atom 을 비눗방울로 추상화 할 수 있습니다. 우리가 만드는 Web Application 을 구조화 한다면 그 구조의 상단에 atom 이 비눗방울 처럼 둥둥 떠다니고 있다고 추상화 할 수 있겠습니다. 만약 개발을 하다가 어떤 비눗방울 (상태) 이 필요하다면 해당하는 비눗방울만 쏙 빼서 쉽게 사용할 수 있습니다.

atoms 에는 우리가 사용할 상태 (state) 를 담습니다. 쉽게 말하면, 우리가 전역적으로 사용하길 원하는 state 를 atoms이라는 비눗방울로 띄어서 어디서든지 사용할 수 있도록 만드는 것 입니다.

Atom이 업데이트되면 해당 Atom을 구독하고 있던 모든 컴포넌트들이 새로운 값으로 리렌더링됩니다. 또 여러 컴포넌트에서 같은 Atom을 구독하고 있으면 그 컴포넌트들이 상태를 동일하게 공유합니다.

Atoms는 atom함수를 사용해 생성한다.

const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

Atoms는 디버깅, 지속성 및 모든 atoms의 map을 볼 수 있는 특정 고급 API에 사용되는 고유한 키가 필요하다. 두개의 atom이 같은 키를 갖는 것은 오류이기 때문에 키값은 전역적으로 고유하도록 해야한다. React 컴포넌트의 상태처럼 기본값도 가진다.

컴포넌트에서 atom을 읽고 쓰려면 useRecoilState라는 훅을 사용한다. React의 useState와 비슷하지만 상태가 컴포넌트 간에 공유될 수 있다는 차이가 있다.

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}
function Text() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return <p style={{fontSize}}>This text will increase in size too.</p>;
}

Selectors

Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)다. 상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다. 컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링된다.

Selector 는 파생된 상태(derived state)의 일부를 나타낸다. 파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.

Selectors는 selector함수를 사용해 정의한다.

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';
    return `${fontSize}${unit}`;
  },
});

get 속성은 계산될 함수다. 전달되는 get 인자를 통해 atoms와 다른 selectors에 접근할 수 있다. 다른 atoms나 selectors에 접근하면 자동으로 종속 관계가 생성되므로, 참조했던 다른 atoms나 selectors가 업데이트되면 이 함수도 다시 실행된다.

이 fontSizeLabelState 예시에서 selector는 fontSizeState라는 하나의 atom에 의존성을 갖는다. 개념적으로 fontSizeLabelState selector는 fontSizeState를 입력으로 사용하고 형식화된 글꼴 크기 레이블을 출력으로 반환하는 순수 함수처럼 동작한다.

Selectors는 useRecoilValue()를 사용해 읽을 수 있다. useRecoilValue()는 하나의 atom이나 selector를 인자로 받아 대응하는 값을 반환한다. fontSizeLabelState selector는 writable하지 않기 때문에 useRecoilState()를 이용하지 않는다. (writable한 selectors에 대한 더 많은 정보는 selector API reference에 자세히 기술되어 있다.)

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  const fontSizeLabel = useRecoilValue(fontSizeLabelState);
  return (
    <>
      <div>Current font size: ${fontSizeLabel}</div>
      <button onClick={setFontSize(fontSize + 1)} style={{fontSize}}>
        Click to Enlarge
      </button>
    </>
  );
}

여러 RecoilRoot를 사용하는 경우

RecoilRoot는 여러개가 같이 존재할 수 있고, 각각이 독립적인 atom 상태의 providers/store가 된다. atom은 각 루트에 따라 다른 값을 갖게 되는 것이다. 그리고 이러한 동작은 override를 false로 지정하지 않는 한, 루트가 다른 루트에 중첩될 때 (내부 루트가 외부 루트를 가릴 경우) 동일하게 발생된다. ("속성" 참조)

selector 캐시같은 캐시들은 루트 사이에 공유될 수 있다. Selector 평가는 캐싱이나 로깅을 제외하고는 멱등적(연산을 여러번 적용해도 결과가 달라지지 않음)이어야 하므로 문제가 되지는 않지만, observable하게 되거나, 루트 전체에 걸쳐 중복 쿼리가 캐시될 수도 있다.

예시

import {RecoilRoot} from 'recoil';
function AppRoot() {
  return (
    <RecoilRoot>
      <ComponentThatUsesRecoil />
    </RecoilRoot>
  );
}

NPM

Recoil 패키지는 npm에 존재한다. 안정화된 최신 버전을 설치하려면 아래의 명령어를 실행하면 된다:

npm install recoil

또는 yarn을 사용한다면:

yarn add recoil

참고

https://recoiljs.org/ko/docs/introduction/motivation/
https://tech.osci.kr/2022/06/16/recoil-state-management-of-react/

profile
what doesn't kill you makes you stronger

0개의 댓글