All Articles

Recoil 훑어보기

og-image

image by https://recoiljs.org/

Recoil의 발견 (TL;DR)

이번 React Europe을 다 챙겨본 것은 아니지만, 내용을 찾아보니 Recoil이라는 상태관리 라이브러리가 뜨거운 관심을 받고 있었다. 나는 코드스쿼드 (부트캠프기관)에서 공부하면서 contextAPI와 useReducer를 학습해 상태관리를 진행해봤고, 내 개인 프로젝트는 Redux를 통해 진행하고 있다. 그리고 최근 입사한 회사 서비스에는 운이 좋게도(?) MobX를 사용해 상태관리를 진행하고 있어 다양한 상태관리 라이브러리 / API를 경험하게 되었다. 처음 Global Store라는 개념을 공부하면서 다양한 상태관리 라이브러리를 사용해보고 차이점을 알고 싶은 욕심이 있었지만 당시에는 모든 것을 이해하기 벅찼고, MobX의 경우에는 hooks지원이 미비하여 (당시 React Hooks를 사용) 사용해 보지 못했지만, 지금까지 나도 성장해왔고 라이브러리들도 기술 지원 및 호환성 부분이 계속 업데이트되면서 다양한 기술을 접하면서 당시의 궁금증을 조금씩 해소할 수 있었다. 그리고 이번 Recoil을 접하면서 기존 상태관리 라이브러리들과는 어떤 차이가 있는지 궁금증이 생기기 시작했다. 아직 완벽하게 이해할 수는 없지만 공식문서와 다양한 블로그를 참조하며 Recoil과 다른 상태관리 라이브러리와의 차이점을 정리하고자 한다.

MobX의 경우는 클래스 기반의 라이브러리로 기존 React와 같은 Flux Architecture을 지향하는 라이브러리는 아니기 때문에 MobX는 차이점에 대해 정리하지 않으려 한다.

Recoil의 발생 배경

공식홈페이지에서는 서두에 React 내부 상태관리 API의 한계에 대해 설명하며 Recoil의 특징을 도출하려는 듯 했다. 설명을 요약해보자면, React 내부의 상태관리 API를 사용하는 것이 최고의 방법이라 해도 몇 가지 제한사항이 있다고 한다.

  1. state를 props로 전달해야 하는 점. 그리고 그로 인해 큰 컴포넌트 Tree를 생성하게 될 경우 re-rendering 크 Tree에 포함된 컴포넌트들의 re-rendering 문제점
  2. contextAPI의 특징의 한계점 (하나의 value만 store에 저장)
  3. 이로 인한 코드스프리팅의 한계

Recoil은 이러한 문제를 리액트 스럽게 풀어나가려 한다고 한다. 직접 공식 홈페이지의 Motivation부분에 잘 설명되어 있는데, 그 부분의 가장 아래 Recoil의 특징을 설명한 부분 중 아래의 내용이 굉장히 흥미로웠다.

useState와 비슷하게 Local State로도 사용할 수 있고, Global State로 사용할 수도 있으며, 필요하다면 reducer로 감싸서 사용할 수도 있다.

아직 사용해보지 않아서 모호한 내용이었고, 전부 이해할 수는 없었지만, Introduction에 코드를 통해 이해를 돕고 있었다.

Recoil의 핵심 개념

Recoil은 AtomsSelectors로 이뤄져있다고 한다. 간단하게 설명해보자면, Atoms는 State고 Selectors는 State로 사용할 수 있는 함수 라고 말할 수 있을 것 같다. 물론 이 이상의 의미와 기능이 있겠지만 기존의 간단하게 위와같이 설명할 수 있을 것 같다. 아래에 각각의 특징을 정리해보겠다.

Atoms

Atoms are units of state

Atoms에 대한 정의이다. 아직 정확하게는 사용해보지 않아서 각각의 state 집합으로 이게 store의 느낌인지 단순히 다수의 state를 말하는지는 모르겠다. 하지만 중요한 것은 이 Atoms는 저렇게 개별적으로 선언하고 State가 변경되면 컴포넌트는 re-rendering이 발생하는 기존과 동일한 상태의 개념이다. 그리고 Atoms는 runtime에서 state가 새롭게 생성될 수 있는 특징을 갖고 있다.

Atoms는 컴포넌트 내부에 불러와 사용하며 다른 컴포넌트에서 사용해야한다면 이 state를 동일한 방법으로 불러와 이를 공유할 수 있다고 한다. 먼저 Atoms를 생성하는 방식은 아래와 같다.

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

이와 함께, 공식문서에서는 Atoms의 특징을 아래와 같이 정리했다.

  1. Atoms는 전역으로 unique한 key값을 가져야하며 default value를 가져야한다. (디버깅, 보존성, 특정 API에서 활용)
  2. Atoms를 사용하고 업데이트하기 위해서는 useRecoilState hook을 사용한다. 이는 useState와 굉장히 흡사하지만, useRecoilState를 통해 어떤 컴포넌트에서도 상태를 공유할 수 있다.

글로만 보면 어렵지만, 친절하게 위의 특징을 아래의 코드와 함께 설명하고 있다.

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}

useState와 굉장히 흡사한데, initial state 부분에 단순히 값을 넣는게 아닌 atom로 생성한 state를 전달하면 된다. 그리고 setFontSizesetFontSize(value)와 같이 값을 전달해 state를 업데이트 해도 되고, 위의 코드와 같이 콜백함수를 통해 state 업데이트 하는 모습을 볼 수 있다. 거의 useState의 사용법과 흡사하다.

function Text() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return <p style={{fontSize}}>This text will increase in size too.</p>;
}

그리고 위와 같이 다른 컴포넌트인 Text 컴포넌트에서 사용한다면 동일하게 선언하여 state를 공유할 수 있다고 한다. 굉장히 편리해 보인다.

Selectors

A selector is a pure function that accepts atoms or other selectors as input.

Selectors는 Recoil에서 어려운 개념이자 활용도가 높지 않을까라고 생각되는 개념이다. Recoil에서는 SelectorsAtoms와 다른 Selectors를 input으로 받는 순수 함수라고 정의하며 이를 Atoms와 동일하게 State로 사용할 수 있다고 설명했다. 다시 말해, 기존의 state 또는 업데이트된 state를 계산해 새로운 state를 리턴하는 함수이고 이를 Component에서 state와 동일하게 구독할 수 있는 특징을 갖고 있다. 공식 문서에서는 state를 최소화하고 state의 변화를 추적하며 재 계산하고 이를 참조하는 각 컴포넌트에 전달해 효율을 높였다고 설명했다.

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

    return `${fontSize}${unit}`;
  },
});

공식문서의 코드를 보면, fontSizeLabelStateSelectorsselector 함수의 arguments로 Atoms과 마찬가지인 unique한 key값과 computed되는 로직이 작성된 get property를 포함한 객체를 받고있다. 그리고 fontSizeState이름의 Atoms을 참조하고 이를 px과 결합시켜 string을 return하고 있다. 이를 컴포넌트에서 사용할땐 아래와 같이 사용하면 된다.

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>
    </>
  );
}

SelectorsuseRecoilValue를 사용해 각 컴포넌트에서 참조할 수 있고 Selectors뿐만 아니라 Atoms 값을 참조할 때도 사용할 수 있는데, API 문서에서는 단순히 값을 읽어오는 경우에만 사용하는 것을 권장했다.

Recoil에 대한 나의 생각

아직 많은 기능이 내포되지 않은 이유도 있겠지만, Recoil은 대체로 굉장히 심플하고 간단해 러닝커브가 작을 것이라는 느낌을 강하게 받았다. 리덕스의 경우에는 dispatch와 action, reducer 등이 어떻게 상호 동작하는지 등을 처음에 이해하는게 굉장히 어려웠지만, recoil은 개념과 API가 적어 이해하는데 상대적으로 쉬운 느낌을 받았다. 또한, 따로 Store를 정의하거나 다수의 Store들을 합치고 이를 Provider의 props로 전달하는 등 복잡한 작업이 생략되어 더욱 러닝커브가 작고 쓰기 편하다는 느낌을 받았다. 그리고 무엇보다 Selectors를 활용해 State를 최소화하고 단순히 다른 State와의 결합, 계산의 결과물로 선언해 뒀었던 State를 함수로 직접 사용할 수 있는 점이 기존의 상태관리 라이브러리들과 다른점이자 효율적인 측면이 될 수 있다고 생각했다. 어떤 면에서는 MobX 느낌도 얻을 수 있었다.

이제 막 수면위로 올라왔기에 더욱 지켜봐야하겠지만, 기존의 상태관리 라이브러리들을 업그레이드한 느낌의 새로운 라이브러리이기에 충분히 주목받을만 하다고 생각했다. 충분히 시간을 갖고 사용해보며 다음 프로젝트에 적용해볼 생각이다.

도움받은 문서

recoiljs
Harsh Makadia Blog

공부한 내용을 정리하는 공간으로 학습 중 습득한 내용이 정확하지 않은 정보를 포함할 수 있어 추후 발견시 수정하도록 하겠습니다.