이번 글에서는 recoil의 사용 방법이 아니라, recoil이 recoil을 구현한 방법을 위주로 살펴볼 예정이다.
1. Recoil ?
우선, recoil에 대해 알아보자. recoil은 페이스북에서 만든 리액트 상태관리 라이브러리이다.
아직 정식 버전인 1.0.0을 출시하지 않은 것을 확인할 수 있다. recoil팀에서는 리액트 18에서 제공되는 동시성 렌더링, 서버 컴포넌트, Streaming SSR 등이 지원되기 전까지는 1.0.0을 릴리스하지 않을 것이라고 밝힌 바 있다. 따라서, 아직 Recoil을 운영 환경에서 사용하기에는 안정성, 성능, 사용성 등이 보장할 수 없다.
하지만, recoil은 다른 상태관리 라이브러리에 비해 사용하기 쉬워 많은 사랑을 받고 있다.
Recoil의 사용법
recoil은 사용하기 간단하다.
RecoilRoot로 애플리케이션을 감싼다.
Atom을 만든다. Atom은 상태의 일부로, Atom의 값을 읽는 컴포넌트는 암묵적으로 Atom을 구독한다. Atom에 어떤 변화가 있으면 그 Atom을 구독하는 모든 컴포넌트가 리렌더링된다.
Selector는 atom이나 다른 selector를 입력으로 받는 순수 함수다. 입력이 된 atom이나 selector가 갱신되면 selector 함수도 다시 평가된다.
useRecoilValue를 이용하여 읽을 수 있다.
Reducer의 action과 같은 기능을 한다.
useSetRecoilState를 이용하여 atom값을 변경한다.
Recoil의 등장 배경
store는 ‘외부요인’으로 취급되기 때문에 React의 내부 스케줄러에 접근할 수 없다. 동시성 모드가 등장하며, 이러한 것은 중요해졌다. Redux는 강력한 기능을 제공하지만 기본적인 store 구성을 위해 많은 보일러 플레이트와 장황한 코드를 작성해야 한다. 또한 비동기 데이터 처리 또는 계산된 값 캐시와 같은 중요한 기능은 라이브러리의 기능이 아니며, 이를 해결하기 위해 또 다른 라이브러리를 사용해야 한다.
❓ 동시성 모드가 뭔데?
2022년, 리액트 18이 발표되었다. 이는 성능 향상과 렌더링 엔진 개선에 초점이 맞춰졌다. 리액트 18은 향후 출시된 리액트 기능의 토대가 될 동시성 렌더링 API의 초석을 다졌다.
동시성이란, A를 수행하다가 대기 상태에 걸리면 B를 수행하는 것을 말한다. 여기서 동시에 수행하지 않는다는 점이다. 리액트 18 이전의 렌더링이랑 개입할 수 없는 하나의 동기적 처리였기에 한 번 렌더링이 시작되면 중단할 수 없었다. 동시성은 리액트 렌더링 메커니즘의 근본적인 개선이다. 동시성을 통해 리액트는 렌더링에 개입한다.
동시성 렌더링의 토대가 도입된 덕에 리액트 18에서는 suspense, 스트리밍 서버 렌더링, 변이 같은 새로운 기능이 소개되기도 했다.
Transitions : 급하지 않은 업데이트 상황에서 UI 변화를 표시하는 데 쓸 수 있다
startTransition(callback)
strict mode : 이전 상태 값을 가진 컴포넌트의 mount, unmount, remount를 시뮬레이션
마운팅이 해제되기 전과 같은 컴포넌트 상태를 담은 트리를 리마운트함으로써 이전 화면을 재빨리 마운트한다
recoil의 특징과 장점
Atoms, Selector
atom : 상태를 정의
selector : 상태를 가져오거나 수정
→ 상태를 하나의 중심점에서 관리하여 필요한 상태에만 접근
전역 상태 추적
사용하는 컴포넌트들만 리렌더링하도록 최적화
중앙 상태 관리
선언적 상태 관리
궁금증
1. recoil이 Context API보다 정말 좋을까?
Context API는 Provider 하위의 모든 consumer들은 Provider 속성이 변경될 때마다 다시 렌더링 된다. 즉, Provider의 값이 배열이나 객체인 경우 구조가 조금이라도 변경된다면 그 Context를 구독하고 있는 하위의 모든 것이 렌더링 될 것이다.
모든 이미지를 하나의 Context에 저장할 수 없다. 왜냐하면, 이미지 하나의 이름을 변경하면 모든 것이 다시 렌더링될 것이기 때문이다. 하지만 각각의 이미지가 각각 Context를 가지고 있다면, 새로운 이미지에 Context Provider를 추가하여 컴포넌트 트리를 다시 구성하고 전체 서브 트리를 다시 마운트해야한다. 메모이제이션을 한다해도 이는 한계가 있다.
recoil은 배우기 쉽다. recoil을 사용하기 위해서는 RecoilRoot로 애플리케이션을 감싸고, atom이라는 단위로 선언하여 useState를 recoil의 useRecoilState로 대체하면 된다. 두 번째, 컴포넌트가 사용하는 데이터 조각만 사용할 수 있고, 계산된 selector를 선언할 수 있으며, 비동기 데이터 흐름을 위한 내장솔루션까지 제공한다.
Context API의 리렌더링 이슈
객체의 일부 프로퍼티만 업데이트 된다하더라도 해당 Context를 가져다가 사용하는 모든 Consumer가 리렌더링된다.
상태를 하나의 value로 제공할 경우 불필요한 리렌더링이 야기된다. 만약 상태가 여러 개일 경우 provider의 개수도 그에 따라 그만큼 많아져야 한다. 런타임에 새로운 provider가 필요하다면, 새로운 node를 트리 중간에 삽입해야 하는데, 큰 렌더링 이슈가 발생할 수 있다.
Provider와 Consumer가 강결합된다. 무조건 Provider가 Consumer의 상위에 있어야 하며 이는 Code Splitting을 어렵게 한다.
useMemo와 useCallback을 사용하여 해당 함수와 값이 변경되지 않았다면 불필요한 리렌더링을 방지할 수 있다. 하지만, 컨텍스트 값이 변경되면 해당 값을 사용하는 모든 컴포넌트가 여전히 리렌더링 된다. 또한, 이러한 메모이제이션은 복잡성을 요구하며, 메모리 사용이 성능보다 더 큰 문제가 될 수 있다. 그리고 종속성 배열을 잘 관리해야 한다. 종속성이 잘못 설정되면 예상치 못한 버그나 불필요한 리렌더링이 발생할 수 있다,