Recoil, 이제는 떠나 보낼 시간이다

clockclock
10 min readDec 28, 2023
v0.7.7 기준 작성

개요

React를 사용해 서비스를 개발할 때 상태 관리 라이브러리를 사용하는 것은 매우 보편적인 일이다. React에서 자체적으로 제공하는 useState hook 하나만으로도 상태(state)를 가진 컴포넌트를 구축할 수 있지만, 서비스 규모가 커지고 컴포넌트 간의 관계가 복잡해질 때 상태를 효율적으로 공유하고 관리하기 위해 사용을 하게 된다.

상태 관리 라이브러리는 정말 다양하다.
React를 사용해 서비스를 만들어봤다면 혹은 상태 관리 라이브러리 도입을 고려한 적이 있다면 Redux, MobX, Recoil, Zustand, Jotai 등 여러 상태 관리 라이브러리를 들어본 적이 있을 것이다. 오늘은 그 중 Recoil에 대한 여러 가지 이야기를 하고자 한다.

Redux, MobX

약 2020년 그 당시 React를 사용한 서비스를 개발할 때 상태 관리 라이브러리를 도입한다면 Redux 혹은 MobX의 점유율이 매우 높았다.
Redux와 MobX 모두 서비스에 맞는 UI를 구현하기 위해 동적인 데이터 즉 상태를 효율적으로 관리할 수 있는 장점과 더불어 그 당시에 React와 항상 붙어 다니는 영혼의 단짝이었다. 하지만 그에 따라 몇 가지 리스크들도 있었는데, 보일러 플레이트 코드가 많아지거나 디버깅이 용이하지 않거나 React의 상태 관리 라이브러리를 사용하기 위해 새로운 다른 러닝 커브가생길 수 있기도 했다

What is Recoil?

2020년 Facebook(현 meta)에서 발표한 React의 상태 관리 라이브러리다.
발표와 동시에 많은 이목을 끌었던 이유가 몇 가지 있다.

  1. 커뮤니티
    React를 주도하여 개발한 Facebook(Meta)에서 개발한 React의 상태 관리 라이브러리다. React를 만든 기업에서 만든 React를 위한 라이브러리이기에 React와 호환성이 좋을 것이며, Facebook(Meta)이라는 세계 최고의 IT 회사에서 개발 및 관리를 하기 때문에 유지 보수와 업데이트가 잘 될 것이며 커뮤니티 또한 활성화에 기대감이 컸다
  2. 러닝커브
    Recoil은 다른 상태 관리 라이브러리에 비해 사용방법이 간단하다.
// font.js 
const FontSizeState = atom ({
key: 'fontSizeState' ,
default: 14 ,
});
// FontButton.jsx 
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<Button
onClick = {() => setFontSize((size) => size + 1)}
style={{fontSize}}>
버튼
</Button>
);
}

3. atomic state
기본적으로 상태를 원자(atom)라는 작은 단위로 취급을 하며 작은 상태를 조합하여 큰 상태를 만들고, 또한 상태에서 다른 상태를 파생하거나 Selector와 같은 순수 함수를 제공하며 bottom-up 방식을 사용한다.

이는 개발자로 서비스에 필요한 상태를 조합성이 좋은 작은 상태로 효율적으로 관리할 수 있게 하였고, 상태의 역할을 명확하게 구분 지을 수 있는 등 여러 장점을 가지고 있었다.

기존에 사용하던 상태 관리 라이브러리와는 다른 메커니즘으로 진부했던 상태 관리 라이브러리 생태계에서 큰 인기를 얻었던 점도 있다.

근황

2024년을 바라보고 있는 현재 Recoil을 사용하던 개발자들의 이탈률이 점자 커지고다.

https://npmtrends.com/jotai-vs-recoil-vs-zustand

실제로 다른 상태 관리 라이브러리와 비교해도 다운로드 횟수가 줄어들고 있고, Recoil과 유사한 atom 단위의 상태를 다루는 Jotai에게 역전을 당하기도 했다. 이런 현상이 발생한 몇 가지 이유가 있다.

01. 업데이트 주기
오픈소스의 경우 대부분 모든 개발자 동등하게 개발에 기여할 수 있고, 라이브러리에 대한 토론과 여러 의견을 거쳐 더욱 좋은 라이브러리로 발전한다.

활발히 사용되는 패키지 중에 눈 깜빡했는데 메이저 버전이 올라간 프레임워크도 존재한다.

이에 반해 Recoil의 경우 새로운 버전의 릴리즈 주기다른 유사 라이브러리에 비해 상대적으로 느린 편이다. 근 1년간 패치가 두 번 밖에 되지 않았다. 또한 issue 및 PR이 쌓여가고 있지만 정작 코멘트 하나 없는 이슈가 대부분이다.

https://github.com/facebookexperimental/Recoil/tags

02. memory leak
Recoil을 사용함으로 메모리 누수가 발생했다는 이슈는 어렵지 않게 찾아볼 수 있다.

Recoil에서 atom, selector 등을 node로 저장을 하고 node 간의 관계를 graph에 저장한다. 그리고 store라는 특정 시점의 정보를 가진 값이 있는데 RecoilRoot에서는 이 store를 매 변경 시 생성한다.

// Recoil_Retention.js
function findReleasableNodesInner(searchFromNodes: Set<NodeKey>): void {
...
for (const node of downstreams) {
if (getNode(node).retainedBy === 'recoilRoot') {
nonReleasableNodes.add(node);
continue;
}
if ((storeState.retention.referenceCounts.get(node) ?? 0) > 0) {
nonReleasableNodes.add(node);
continue;
}
...

}

위 소스 코드는 Recoil core 내부에 Rentention.js 모듈에서 node를 release 하기 위한 함수이다. node의 retainedBy가 ‘recoilRoot’가 아닐 때 nonReleasableNodes에 추가가 되고, 이후 if 코드는 실행이 되지 않는다.

// Recoil_Retention.js
function retainedByOptionWithDefault(r) {
// The default will change from 'recoilRoot' to 'components' in the future.
return r === undefined ? 'recoilRoot' : r;
}

// atom, selector
const retainedBy = retainedByOptionWithDefault(options.retainedBy_UNSTABLE);

하지만 atom, seletor를 생성할 때 retainedBy를 할당하기 위해 호출되는 함수의 인자로 전달하는 retainedBy_UNSTABLE 옵션은 현재 UNSTABLE 상태임과 동시에 외부에 공개되어 있지 않아 항상 ‘recoilRoot’로 할당이 된다.

retainedBy_UNSTABLE 옵션을 ‘components’로 할당을 하더라도 동작하지 않는다는 열려있는 이슈 또한 존재한다
주석으로 미뤄보아 지원 예정인 옵션이었으나 아직 3년 전 그대로 사용이 불가능한 상태인 거 같다.

또한 변경되는 동적인 상태(state)에 대한 가장 작은 단위인 atom에서 파생되는 selector, atomFamily, selectorFamily에 캐시 정책을 기본값인 keep-all을 사용하게 되면 이전 상태들이 메모리상에서 해제되지 않고 유지된다.

실제로 Next v13을 사용하는 개발자의 SSR 고려 여부를 묻는 질문과 캐시 관련된 질문에 ‘most-recent’를 사용하라는 Recoil 개발자의 답변이 있다

03. SSR에서의 미비한 지원
메모리 누수가 브라우저에서 발생하는 것은 위험하지만,
SSR 환경에서 즉 서버에서 메모리 누수가 발생한다면 치명적일 수 있다.

실제로 SSR 환경의 Recoil에서 store를 release 하기 위해 patch-package와 같은 오픈소스를 커스텀 하는 도구를 사용해 store 내부의 nodes를 release 하는 함수를 구현해 사용하는 사람도 있다.

또한 다른 라이브러리와 달리 SSR 환경에서도 사용할 수 있는 useHydrate와 같은 메소드도 존재하지 않는다. 때문에 메소드의 런타임의 구분이 명확하지 않기 때문에 typeof window !== ‘undefined’와 같은 코드의 사용이 불가피하다.

그렇다고 SSR 환경을 아예 배제한 것은 아니다. 실제로 Recoil core 중에 isSSR로 런타임별 로직을 분기 처리하는 곳이 다수 존재한다.

하지만 이는 개발자로 하여금 내부적으로 런타임별 동작이 다른 것을 미리 숙지하고 라이브러리를 사용해야 하는데 이는 좋은 DX라고 할 수 없다.

애초에 Recoil 문서 전체에서 SSR을 지원한다는 내용은 존재하지 않을뿐더러, RecoilURLSync를 제외한 어디에도 SSR 단어조차 찾아볼 수 없다.

04. Recoil 개발자의 퇴직
Facebook(현 Meta)에서 팀 리더로 Recoil Recoil-sync Recoil-relay 등을 개발했던 Douglas Armstrong 님이 지난 1월 정리 해고에 의해 퇴직을 하게 되었다. Recoil의 초기 설계부터 개발까지 담당했던 메인 개발자가 사라진 것에 영향이 있을 수 있다.

메인 테이너 한 명이 빠지게 되어서 직접적인 영향이 생겼다는 건 관리 개발과 유지가 잘 되어가던 라이브러리가 아닐 수 있다는 방증이다. 때문에 개발자의 퇴직과 내부 구조조정의 영향이 간접적인 영향 혹은 어쩌면 영향 자체가 없을 수도 있다.

결론

Recoil의 열려있는 이슈들을 보면 Recoil의 지원 중단 혹은 지속적인 유지 보수 여부에 대한 오피셜을 기다리는 사람들이 있는 반면, 다른 라이브러리를 선택하는 사람들도 다수 존재한다.

Recoil을 사용 중이라면 서비스 규모에 따라 다르지만, 코어의 문제가 발생했을 때 해결을 하는 것보단 Jotai로 마이그레이션을 고려해 보는 것도 좋을 수 있다. Recoil을 사용하려 고려 중이라면 아토믹 한 상태를 다루면서 비슷한 문법과 활발한 업데이트 그리고 SSR을 공식 지원하는 Jotai 혹은 zustand를 추천 하고싶다.

본인도 Recoil을 즐겨 사용했었고, 현재는 Recoil을 굳이 도입하지 않는다. Recoil을 사용했던 개발자 입장에서 훌륭한 경험을 했던 것으로 만족한다

사용자의 이탈이 많아지면 더욱 편리한 무엇인가 탄생하고
간혹 새로운 패러다임을 가져온다고 믿는다.

참조

--

--