Recoil의 개념과 사용법

이전에는 상태 관리를 위해 주로 Redux를 사용했었는데 Recoil에 알게 되어 지금 진행 중인 프로젝트에서는 recoil을 쓰고 있다. Recoil을 사용하는 이유와 장단점, 사용법에 대해 작성해보려 한다.

사용하는 이유

외부 상태 관리 라이브러리보다 React에 내장된 상태 관리 기능을 사용하는 것이 좋지만 React는 몇 가지 한계가 있다.

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

Recoil은 직교하지만 본질적인 방향 그래프를 정의하고 React 트리에 붙인다. state의 변화는 이 그래프의 atoms로부터 selectors를 거쳐 컴포넌트로 전달되며, 다음과 같은 접근 방식을 따른다.

  • 공유 state도 React의 local state처럼 간단한 get/set 인터페이스로 사용할 수 있도록 boilerplate-free API를 제공한다.
  • 동시성 모드를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 갖는다.
  • state 정의는 점진적이고 분산되어 있어서 코드 분할이 가능하다.

Redux를 사용할 땐 하나의 state를 저장하기 위해 작성해야하는 코드가 많았다. 하지만 recoil은 비교적 적은 양의 코드만을 작성해 사용할 수 있다.

개요

Recoil을 사용하면 atoms에서 selectors를 커쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다. Atoms는 컴포넌트가 subscribe할 수 있는 state의 단위이며, selectors는 atoms의 상태값을 동기 또는 비동기 방식을 통해 변환한다.

예시

예시는 탑승하는 인원의 성인 인원과 아동 인원의 state를 관리하는 내용이다.

디렉토리 구조는 여러 레퍼런스를 찾아본 후에 다음과 같이 src 하위의 recoil 폴더 하위에 atoms와 selectors 폴더를 만들기로 결정했다.

선택한 성인과 아동의 탑승인원을 관리하는 atoms와 총 탑승인원을 관리하는 selectors로 구성되어 있다.

src
├─recoil
│  ├─atoms
│  │  └─headcountsState.ts
│  └─selectors
│     └─totalHeadcountsState.ts
├─components
...

Atoms

Atoms는 상태의 단위이며, 업데이트와 subscribe이 가능하다. atom이 업데이트되면 해당 state를 사용하는 컴포넌트는 업데이트된 값을 반영해 렌더링된다. Atoms는 React의 로컬 컴포넌트의 상태 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 state를 공유한다.

Atoms는 atom 함수를 사용해 생성한다. 다음과 같이 초기값을 선언 후에 atom 내의 default 값으로 주었다.

// headcountsState.ts
const initialState = {
  ADULT: 0,
  CHILD: 0,
}

export const headcountsState = atom({
  key: "headcountsState",
  default: initialState,
});

Hook

컴포넌트에서 atom을 읽고 쓰려면 useRecoilState 라는 훅을 사용한다. 성인과 아동의 인원과 플러스 버튼과 마이너스 버튼을 합칠 수 있지만 간단하게 다음과 같이 일부만 작성했다.

const ChildHeadcountPlusButton = () => {
  const [headcounts, setHeadcounts] = useRecoilState(headcountsState);

  return (
		<button 
			className="child-headcount-plus-button"
			onClick={() => {
				setHeadcount({
					CHILD: headcounts.CHILD + 1,
					ADULT: headcounts.ADULT,
				})
			}
		/>
	)
}

인원 정보를 사용하는 다른 컴포넌트에서 setter만 필요하거나 valuer만 필요하면 다음과 같이 useRecoilValueuseSetRecoilState 등의 hook을 사용할 수 있다.

const headcounts = useRecoilValue(headcountsState);
const setHeadcounts = useSetRecoilState(headcountsState);

state를 초기화해주는 useResetRecoilState 훅도 있어 초기값으로 돌려야할 때 의미없는 값을 주거나 초기값을 초기화하는 곳에서 알고 있지 않아도 된다.

const resetHeadcounts = useResetRecoilState(headcountsState);

Selectors

selectors는 state를 기반으로 파생된 데이터를 계산하는 데 사용된다. 최소한의 state만을 atoms로 저장하고, 그로부터 계산해야하는 데이터는 필요할 때마다 계산하거나 또 다른 atoms로 저장할 필요 없이 selectors로 저장하면 된다. 다음은 headcountsState의 성인 인원과 아동 인원 수를 합해 전체 인원 수를 제공해주는 selector이다.

// totalHeadcountsState.ts
export const totalHeadcountsState = selector({
  key: "totalHeadcountsState",
  get: ({ get }) => {
    const headcounts = get(headcountsState);
    return headcounts.ADULT + headcounts.CHILD;
  },
});

전체 인원 수가 필요한 곳에서 util 함수를 사용해서 전체 인원 수를 항상 계산해주거나 할 필요없이 다음과 같이 위의 selector만 호출하면 사용이 가능하다.

const totalHeadcounts = useRecoilValue(totalHeadcountsState);

사용 후기

recoil을 사용해보니 상태 관리 라이브러리를 사용하지 않거나 redux를 사용할 때보다 훨씬 간결한 코드만으로 상태 관리를 할 수 있게 되었다.

state를 여러번 전달하지 않고 하위 컴포넌트에서 호출해줌으로써 전달해야하는 props를 줄일 수 있었다. selectors를 사용함으로써 state로부터 파생된 데이터를 사용하기에도 편했다.

페이지를 새로고침하거나 이동할 때 값이 초기화되지 않아야 하는 경우도 있는데, atom에 localStorageEffect 또는 sessionStorageEffect를 effect로 추가하면 스토리지에 저장하는 것도 간단하게 할 수 있다. 이를 사용하는 법은 다음에 다뤄보려 한다.


Written by@jaeeun
I explain with words and code. I explain with words and code. I explain with words and code.