이 글은 recoil 현재버전 v0.5.2기준으로 작성되었습니다.
이번 프로젝트 개편 때 Recoil(이하 리코일)을 메인 상태관리 라이브러리로 사용하려고 하고 있다. 리코일을 사용하려는 이유는 일단 React를 만든 Facebook(이제는 Meta)에서 진행하는 프로젝트인 만큼 믿을 수 있다고 생각했고, 인터페이스가 굉장히 간결하여 자유도가 높았다. 이러한 간결한 인터페이스를 사용하면서 다양하고 생산성이 높은 형태의 코드를 만들 수 있을 것으로 봤다.
리코일이 아직 버전 1이 나오기 전이므로 학습 목적 겸, 프로젝트에서 사용할 때 도움이 될 겸사겸사 리코일 소스를 쭉 읽어봤다. 아직 소스 전체를 이해하지는 못했지만 대략적으로 이해는 했는데…
결론부터 말하자면… 리코일은 메모리 누수 문제가 있다.
문제가 발생하는 예제를 알아보자.
예제를 만들 필요가 없다. 공식 사이트에 있는 selectorFamily 를 사용하여 비동기 호출하는 예제다. 이 예제는 메모리 누수가 발생한다. userID가 같을 때에는 값이 변경되지만 userID가 변경된다면 값이 새로 쓰인다. 기존 값은 사용하지 않더라도 지워지지 않는다. 즉, 데이터가 누적되고 메모리 누수가 발생한다.
가장 문제가 심한게 selectorFamily이고, atomFamily와 atom, selector도 값을 사용하지 않더라도 값이 캐싱된 상태로 계속 메모리에 남아있다.
리코일의 구조에 대해서 간략하게 알아보자.
리코일의 저장소를 알아보면 기본적으로 atom, selector가 있고, 매개변수를 받아서 atom과 selector를 만들어주는 atomFamily, selectorFamily가 있다.
내부적으로는 이런 변수들은 node(이하 노드)라는 형태로 저장이 되고, 이 노드에 대한 관계를 graph(이하 그래프)에 저장한다. 그리고 이런 노드와 그래프 들이 최상위 store(이하 스토어)에 저장이 된다. 스토어는 특정 시점의 정보라고 보면되고 RecoilRoot는 이 스토어를 매 변경시에 찍어내게 된다. 이 찍어내는 스토어에 접근하는게 snapshot이라고 보면 된다.
리코일은 RecoilRoot에서 현재 스토어와 노드, 그래프 정보를 모두 가지고 있다. 컴포넌트에서 리코일 값을 사용하거나 제거할 때 ReferenceCount 정보도 저장하는데, 이 값이 의미가 없다.
아래는 메모리를 해제하기 위해 해제 가능한 노드를 찾는 코드다. 코드가 길지만 결국 현재 릴리즈 버전은 모두 첫번째 if문을 넘지 못한다.
atom이나 selector, atomFamily, seletorFamily 코드를 보면아래 함수를 호출하여 retainedBy를 만드는데 옵션은 현재 외부로 공개되어 있지 않아서 항상 recoilRoot로 지정된다.
Recoil 에서는 이런 문제를 모를까?
리코일 깃헙에 memory 라고만 검색해봐도 몇개의 이슈가 보인다.
https://www.facebook.com/groups/react.ko/posts/2835701603356660/
그리고 메모리 관련 이슈를 찾아보다보니 0.2.0버전에도 있던 문제로 보인다. (지금은 v0.5.2인데… 언제 수정을 할까?)
해결방법은 아예 없을까?
1. RecoilRoot를 다시 그리기
RecoilRoot 에는 아래와 같은 코드가 있다. 즉, RecoilRoot가 unmount되는 순간 모든 데이터는 해제된다.
하지만 RecoilRoot 특성상 이걸 다시그리려면 전체페이지를 다시그려야 한다.
2. selectorFamily 한정 cachePolicy_UNSTABLE 옵션 사용
selectorFamily 가 가장 문제가 심각해서 그런지… selectorFamily에는 cachePolicy 옵션이 추가되어 있다. 기본값은 keep-all이기 때문에 항상 유지되고 lru나 most-recent를 사용하면 메모리를 조금은 관리할 수 있다.
물론 UNSTABLE이다. 즉 안정적이지 않다.
3. retainedBy 의 기본 옵션을 변경한다.
위에서 아래 소스를 보면 주석이 눈에 띈다. recoilRoot에서 components로 미래에는 바꿀 예정이다.
components로 값을 변경하면 아래의 코드가 정상적으로 실행되면서 메모리가 해제된다.
리코일 내부에서 retainedBy를 변경하여 테스트를 만들었는데, components를 사용할 때 메모리가 해제되는 테스트 코드들이 있는데 잘 동작하는 것을 볼 수 있다.
그러면 components로 값을 변경할 수 있는 방법은 아래처럼 하면 된다.
- recoil 소스를 fork하여 기본값을 변경한다.
- 타입정의도 안되어 있긴하지만 atom, selector, atomFamily, selectorFamily에는 retainedBy_UNSTABLE 라는 옵션이 있다. 이 옵션에 components를 넣으면 반영된다.
물론 이 방법도 UNSTABLE한 스펙을 사용하는 것이다. 그러므로 느릴 수도 있고, 다른 문제가 있을 수도 있다.
정리
리코일은 메모리 누수가 존재한다. 하지만 해결하는 방법을 찾아가고 있는 것 같긴하다. 현재의 메모리 누수를 어느정도 감내하고 사용하려면 selectorFamily에 cachePolicy_UNSTABLE 옵션을 사용하는걸 추천한다.