Front-end Developer

0%

Recoil

상태 관리

상태 관리가 되려면 아래 기능을 할 수 있어야 한다.

  • 최초값 저장
  • 현재 값 읽기
  • 값을 업데이트

Recoil은 왜 필요할까?

  • React의 내장 상태관리 기능의 한계점 극복
  • 최대한 react스러운 API 유지
  • 사용하기 위한 부속 라이브러리 최소화

React 상태 관리 로직의 한계점

  • 컴포넌트의 상태는 공통되는 부모 컴포넌트까지 올라가야 하고, 심할 경우 어플리케이션 상단까지 올라아가햔다.
  • Context API는 확정되지 않은 수의 값을 저장하는데 적합하지 않고, 최적화 관점에서 한계점이 명확하다. state관리보다는 의존성 주입의 개념에 가깝다고 볼 수 있다.

Recoil의 접근 방법

  • React Tree에 직교되는 형태로 존재하는 방향 그래프로 구성되어 있다. 예를 들어 Redux를 쓰려면 react-redux를 사용해서 react와 연결시켜주어야 했으나 Recoil은 그런 장치가 필요없다.
  • 상태의 변경은 이 그래프를 따라 React component로 흘러들어간다. 따라서 component의 로직을 건드리지 않아도 상태 데이터를 단독으로 변경할 수 있다.

Recoil의 철학

  • 보일러 플레이트가 적다.
  • React의 로컬 상태와 유사한 간단한 인터페이스
  • Concurrent Mode와 호환
  • 코드 상호간의 낮은 결합도를 통해 Code spliting 용이성 확보
  • 파생 데이터를 사용함으로써 데이터를 사용하는 컴포넌트에서 임의로 데이터를 바꾸는 로직을 가져가지 않아도 된다. 가져와서 useEffect로 바꿔주기를 하지 않고, 로직 자체를 Recoil atom에 귀속시킬 수 있다.

Core concept

유연하게 상태 관리 가능

공통적으로 필요한 데이터를 어떻게 저장할 것인가?

  • ContextAPI를 사용하면 다이나믹하게 구성할 수 없고, Coupling이 발생한다.
  • Provider가 추가될 떄마다 Tree는 다시 reconciling을 해줘야 하는 이슈
  • React의 로컬 컴포넌트 state와 동일하게 batching와 같은 작업들이 모두 라이브러리 내부에서 처리된다.

파생 데이터 생성이 용이

  • 상태와 관련 있거나 상태로부터 만들어진 것들
  • 상호 의존적인 state를 만들 필요가 없다. (두 개 이상의 atom을 참고한 또 하나의 atom..)
  • pure function으로 atom 데이터를 사용할 수 있도록 해준다. 정말 데이터에 변화가 있을 때만 recompute 하도록 해준다.

어플리케이션 단의 상태 observing 가능


설치

1
2
3
4
5
npm install recoil

or

yarn add recoil

ESLint

eslint-plugin-react-hooks을 사용하는 경우 아래와 같이 설정한다. useRecoilCallback()을 사용하기 위해 전달된 종속성이 잘못 지정되었을 때 경고를 표시하고 해결 방안을 제시하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 수정된 .eslint 설정
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useRecoilCallback"
}
]
}
}

React

1
2
3
4
5
npx create-react-app my-app

npm install recoil
or
yarn add recoil

RecoilRoot

부모 트리 어딘가에 RecoilRoot가 필요하다. 보통 루트 컴포넌트가 적절하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Root Component

import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil';

function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}

atoms

  • 상태(state)의 일부
  • 데이터를 보관하는 기본 단위
  • 업데이트와 구독이 가능하다. atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 구독한다.
  • React의 로컬 컴포넌트 상태 대신 사용할 수 있다.
  • 동일한 atom이 여러 컴포넌트에서 사용되면 모든 컴포넌트는 상태를 공유한다. atom이 업데이트 되면 각각의 구독된 컴포넌트가 새로운 값을 반영하여 다시 렌더링된다.
  • 어느 컴포넌트에서나 읽고 쓸 수 있다.
  • Redux로 따지면 Reducer와 같이 전체 store의 일부분을 차지하지만 훨씬 적은 보일러 플레이트를 차지하고 작은 단위로 관리할 수 있다.
1
2
3
4
const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
});
  • key: 디버깅, 지속싱 및 모든 atoms의 map을 볼 수 있는 특정 고급 API에 사용되는 고유한 키가 필요하다.
  • key값은 전역적으로 고유해야 한다. 2개의 atom이 같은 키를 갖는 것은 오류이다.
  • React 컴포넌트의 상태처럼 기본값을 가진다.

useRecoilState

  • atom을 읽고 쓰게 하기 위해서 사용한다.
  • 컴포넌트에서 atom을 읽고 쓸 때 사용한다. React의 useState와 유사하지만 컴포넌트 간에 공유될 수 있다.는 차이점이 있다.
1
2
3
4
5
6
7
8
9
10
11
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button
onClick={() => setFontSize((size) => size + 1)}
style={{ fontSize }}
>
Click to Enlarge
</button>
);
}

버튼을 클릭하면 버튼의 글꼴 크기가 1만큼 증가하며, fontSizeState atom을 사용하는 다른 컴포넌트의 글꼴 크기도 같이 변화한다.

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

atomFamily

  • writable한 recoilState atom을 반환하는 함수를 반환한다.
  • atom들의 모음집으로 저장한다. 기본적으로 Recoil 내부적으로 Caching와 같은 최적화를 진행해준다.
  • 일반적으로 atom은 RecoilRoot 단위로 등록이 되지만, 여기서 atomFamily는 사용처가 약간 다르다. 예를 들어 UI Prototyping 툴을 만들 때, 각각의 UI element에 대해 position이나 width, height와 같은 값들을 가지고 있다고 가정할 때, 이런걸 리스트로 보관하면서 memoizing해도 되지만 atomFamily를 사용하면 Recoil에서 이런 부분들을 처리해준다.

selector

  • 파생된 상태(derived state)의 일부를 나타내고 이는 상태의 변화를 의미한다.
  • 순수 함수이다.
  • 상위의 atoms나 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다.
  • 컴포넌트들은 selectors를 atoms처럼 구독할 수 있고, selectors가 변경되면 컴포넌트들도 다시 렌더링된다.
  • selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다. 최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생되는 데이터는 selectors에 명시한 함수를 통해 효율적으로 계산하는 방식으로 쓸모없는 상태의 보존을 방지한다.
  • 어떤 컴포넌트가 자신을 필요로 하는지 자신은 어떤 상태에 의존하는지 추적하기 때문에 함수적이 접근방식을 매우 효율적으로 만든다.
  • 컴포넌트의 관점에서 selectors와 atoms는 동일한 인터페이스를 가지고, 서로 대체할 수 있다.
  • atom, 다른 selector들을 조합할 수 있다.
  • 파생되는 상태를 생성한다.
  • dependency에 해당되는 atom이 업데이트되면 같이 업데이트 되기 때문에 관리의 부담이 없다.
1
2
3
4
5
6
7
8
9
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({ get }) => {
const fontSize = get(fontSizeState);
const unit = 'px';

return `${fontSize}${unit}`;
},
});
  • get: 계산될 함수. 전달되는 get 인자를 통해 다른 atoms, selectors에 접근할 수 있고, 이때 자동으로 종속 관계가 생성되므로 참조했던 다른 atoms나 selector가 업데이트되면 이 함수도 다시 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>
</>
);
}
  • fontSizeLabelState: selector는 fontSizeState하는 하나의 atom에 의존성을 갖는다.
    • 이 selector는 fontSizeState를 입력으로 사용하고 형식화된 글꼴 크기 레이블을 출력으로 반환하는 순수 함수처럼 동작한다.
    • useRecoilValue()를 사용해서 읽는다.
    • writable하지 않기 때문에 useRecoilState()를 이용하지 않는다.
  • 위 FontButton 예제에서 버튼을 클릭하면 버튼의 글꼴 크기가 증가하면서도 현재 글꼴 크기를 반영하도록 글꼴 크기 레이블을 업데이트하는 두 가지 작업이 수행된다.

React에서 사용하는 API

  • useRecoilValue: 기존에 사용하던 리액트의 로컬 상태 API와 동일한 형태로 사용 가능
    • atom, selector 모두 동일한 API를 사용하기에 변경이 필요할 때 언제는 Component 수정을 최소화하고 Recoil State를 변경할 수 있다.
    • 하나의 atom이나 selector를 인자로 받아 대응하는 값을 반환한다.
  • useRecoilCallback: 리액트의 useCallback과 유사하면, 다만 recoil state를 사용할 수 있는 API를
    • atom이나 selector가 업데이트 되었을 때, react componet를 리렌더하고 비동기적으로 recoil state를 읽는다.
    • Render-time에 하고 싶지 않은 시간이 오래걸리는 비동기 액션을 수행한다.
    • Recoil state를 read하거나 write하는 side-effect를 수행한다.
    • render-time에는 어떤 atom이나 selector를 업데이트하고 싶은지 알 수 없다
      • 이 경우에는 useSetRecoilState를 사용할 수 없기 떄문이다.
  • useRecoilCallback은 atom, selector state에 대한 스냅샷을 가지고 있기 때문에 특정 상태값을 사용하고 싶지만 deps에 반영하고 싶지 않을 때 유용하게 사용할 수 있다.
  • 대표적으로 logger와 같은 케이스에서 유용하게 사용할 수 있다.

왜 Recoil인가?

  • 낮은 진입장벽, 간단한 API (Redux의 Action, Store, Reducer와 같은 개념을 모두 익힐 필요 없다.)
  • React API와 굉장히 유사하여 대체하기가 용이하다.
  • 비동기 데이터 처리가 용이하다.
  • atomFamily, selectorFamily를 이용하여 국소적으로 사용하는 상태를 생성할 때 용이하다.
  • atomEffects 등을 이용하여 변화하는 값을 atom 자체적으로 트래킹할 수 있다.

Recoil의 한계점

  • atomEffect를 비롯한 많은 API가 여전히 실험적이다
  • 서버 사이드 렌더링 환경에서 안정성이 미흡하다.

References
왜 Recoil을 써야 하는가?