ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Recoil] Recoil 고수의 길 - snapshot과 useRecoilCallback
    Front-End(Web)/React - 라이브러리들 2023. 10. 5. 17:44
    반응형

    지난번, atomFamily와 selectorFamily 포스팅에 이어, Recoil에서 유용하게 쓰일법한 기능을 하나 더 소개하고자 한다.

    Recoil 상태들의 한 순간인 snapshot, 그리고 이를 다루는 useRecoilCallback 등 다양한 API 기능들에 대해 알아보자!


    🔗 기능들을 소개하기 앞서..

    snapshot과 관련된 기능들을 소개하기 앞서, 이를 소개하게 된 배경들을 간단히 공유해볼까 한다.

     

    1. useRecoilState와는 다른 atom 활용방법

    이전 포스팅에서, Recoil의 atomFamily, selectorFamily 등에 대해 다뤄보았다. (포스팅 링크)

     

    [Recoil] Recoil 고수의 길 - atomFamily, selectorFamily

    최근 프로젝트들을 진행하면서, Validator 솔루션에 Recoil을 적극 채용하면서 이런저런 기능들을 시도해봤던 것 같다. 그 과정에서 유용하게 사용한 기능들에 대해, 정리 및 부가학습 차원에서 포

    abangpa1ace.tistory.com

    글 하단의 사용예시를 참고해보면, Family들의 값을 모으기 위해 Selector를 사용하였었다.

     

    하지만, 이 종합값이 여러 곳에서 사용되는 공통상태가 아니라면 구지 Selector로 만들 필요가 있을까? 필요한 컴포넌트만 계산해서 사용할 수 없을까?

    이러한 고민들 속에서, 복수의 atomFamily를 직접 map() 순회하는 방법을 시도했고, 당연히 아래처럼 참교육을 당했다. 😅

     

    다른 방법은 없을까 리서치를 해보던 도중, snapshot 기능을 통해 현재 atomFamily들을 가져올 수 있었다.

    const snapshot = useRecoilSnapshot();
    console.log(keys.map(key => snapshot.getLodable(validatorFieldDataState(key))));

     

    2. Recoil의 스토어

    Redux와 같은 상태관리 라이브러리들은, 대게 store라고 하는 전역 저장소(트리)에서 상태값들을 저장하고 관리한다. (참고문서)

     

    Redux 시작하기 | Redux

    소개 > 시작하기: Redux를 배우고 사용하기 위한 자료

    ko.redux.js.org

     

    하지만, Recoil을 사용해봤다면 느꼈겠지만, 좀 더 각각의 사용개소에 분포된 느낌이고 그만큼 store의 존재성이 모호하다.

    그 대신, useRecoilCallback과 여기서 획득되는 snapshot 같은 기능들을 통해 우리는 전역상태를 가져다 쓰는 느낌을 좀 더 받을 수 있다.

     

    이러한 배경을 착안해서, 스냅샷과 관련 API들에 대해 알아보도록 하겠다!


    🔗 snapshot(스냅샷)

    React의 모든 상태가 그렇듯, Recoil의 atom의 상태도 우리가 사용하기에 따라 끊임없이 변하게 된다.

    snapshot(스냅샷)은 계속해서 변하는 이 상태들의 한 순간이다. atom 상태의 히스토리 탐색 및 관리 등에 유용하다.

    스냅샷의 인터페이스는 아래와 같다.

    class Snapshot {
      getLoadable: <T>(RecoilValue<T>) => Loadable<T>;
      getPromise: <T>(RecoilValue<T>) => Promise<T>;
    
      map: (MutableSnapshot => void) => Snapshot;
      asyncMap: (MutableSnapshot => Promise<void>) => Promise<Snapshot>;
    }
    • getLodable : 스냅샷 상태를 동기적으로 확인할 수 있는 메서드
    • getPromise : 스냅샷 상태를 비동기적으로 확인할 수 있는 메서드
    • map : 스냅샷을 동기적으로 수정할 수 있는 메서드
    • asyncMap : 스냅샷을 비동기적으로 수정할 수 있는 메서드

    * 스냅샷은 기본적으로 값을 변경할 수 없는 불변 객체인데, 위 map, asyncMap 등으로 상태값에 대한 수정이 가능하다.

     


    🔗 스냅샷 얻기

    Recoil은 현재의 상태를 기반으로 스냅샷을 얻기 위해 아래 hook들을 제공한다. 이들을 자세히 알아보자.

    • useRecoilSnapshot() : 스냅샷에 동기 엑세스
    • useRecoilCallback() : 스냅샷에 비동기 엑세스
    • useRecoilTransactionObserver_UNSTABLE() : 모든 상태 변경에 대해 스냅샷 구독

     

    1. useRecoilSnapshot

    import { useRecoilSnapshot } from 'recoil';
    import { exampleAtom } from '@/recoil/example';
    
    function Example() {
      const snapshot = useRecoilSnapshot();
      console.log(snapshot.getLodable(exampleAtom));
    
      return (
        <p>example</p>
      );
    }

    useRecoilSnapshot 훅은 렌더링 중 동기적으로 스냅샷 객체를 리턴한다.

    단, 이 훅은 스냅샷 상태가 변할때마다 생성되므로, 해당 컴포넌트 또한 그 때마다 리렌더링이 트리거 될 것이다.

    (간단히 사용할 수 있지만, 보다시피 성능 이슈에 유의해야 한다.)

     

     

    2. useRecoilCallback

    type CallbackInterface = {
      snapshot: Snapshot,
      gotoSnapshot: Snapshot => void,
      set: <T>(RecoilState<T>, (T => T) | T) => void,
      reset: <T>(RecoilState<T>) => void,
      refresh: <T>(RecoilValue<T>) => void,
      transact_UNSTABLE: ((TransactionInterface) => void) => void,
    };
    
    function useRecoilCallback<Args, ReturnValue>(
      callback: CallbackInterface => (...Args) => ReturnValue,
      deps?: $ReadOnlyArray<mixed>,
    ): (...Args) => ReturnValue

    React의 useCallback과 문법이 유사하다. 첫 번째 인자(callback)는 메모이즈된 함수, 두 번째 인자는 의존성(deps)을 전달한다.

    메모이즈된 함수는, 스냅샷을 포함해 Recoil 상태를 핸들링할 수 있는 기능들을 제공한다. (CallbackInterface 참고)


    또, 해당 훅은 비동기적으로 Recoil 상태를 업데이트 할 수 있기에 다양한 상태에서 사용된다. 공식문서(링크)는 아래 예시들을 제시한다.

    • atom 및 selector 갱신 시, 이를 구독하면서 컴포넌트 리렌더링이 발생하지 않으면서 상태값을 비동기적으로 읽을 때
    • 렌더링 시점에 실행하지 않아도 되는 복잡한 Recoil 상태 업데이트 지연
    • Recoil 상태 및 업데이트에 대한 side-effects 검증
    • 렌더링 시점에 상태를 업데이트하지 않고(useSetRecoilState), 동적인 시점에 업데이트하기 위한 목적
    • 렌더링 전 데이터 pre-fetching

     

    * 사용예시

    import { useRecoilCallback } from 'recoil';
    
    function Example() {
      const [snapshotList, setSnapshotList] = useState([]);
      const updateSnapshot = useRecoilCallback(({ snapshot }) => () => {
        setSnapshotList(prevList => [...prevList, snapshot]);
      });
    
      return (
        <div>
          <p>Snapshot count: {snapshotList.length}</p>
          <button onClick={updateSnapshot}>스냅샷 저장</button>
        </div>
      );
    }

    해당 예시 역시 useRecoilSnapshot 예시와 유사하게 스냅샷을 획득하고 있다.

    하지만, useRecoilCallback이 메모이즈된 함수이기 때문에, 모든 스냅샷에 대해 리렌더링이 트리거되지 않는다.

    버튼 클릭 시 명시적으로 실행되는 updateSnapshot에서, 상태값을 업데이트하므로 이 경우에 대해서만 리렌더가 발생할 것이다.

     

     

    3. useRecoilTransactionObserver_UNSTABLE()

    function useRecoilTransactionObserver_UNSTABLE(({
      snapshot: Snapshot,
      previousSnapshot: Snapshot,
    }) => void)

    useRecoilTransactionObserver 훅은, 전달된 콜백함수아톰상태가 변경될 때 마다 실행되는 함수다. (useEffect와 유사한 느낌)

    보시다시피, 현재 버전에서 아직 UNSTABLE 기능이기 때문에, 개발 디버깅 및 히스토리 빌드 등의 용도로 권장된다.

     

     

     

    🔗 스냅샷으로 상태값 관리하기

    위에서 Recoil의 스냅샷을 가져오는 3가지 API를 알아보았다. 이제 스냅샷을 통해 상태값을 조회하거나 변경하는 방법들을 소개해보겠다!

     

    - 상태값 가져오기

    맨 처음 소개했던 스냅샷의 인터페이스에서, 아래 getLodable, getPromise 2가지 메서드로 값을 확인할 수 있다.

    • getLodable : 스냅샷 상태를 동기적으로 확인할 수 있는 메서드
    • getPromise : 스냅샷 상태를 비동기적으로 확인할 수 있는 메서드

    예시는 getLodable 기준이다. 사용법은 둘 다 동일하지만, 보통은 getLodable로 충분하고 비동기 Selector라면 getPromise를 사용하면 된다.

    function Example() {
      const getSnapshot = useRecoilCallback(({ snapshot }) => async () => {
        const value = await snapshot.getLodable(exampleAtom);
        console.log("Value: ", value);
      });
    
      return (
        <div>
          <button onClick={getSnapshot}>스냅샷 조회</button>
        </div>
      );
    }

     


    - 상태값 수정하기

    앞서 언급했지만, 기본적으로 스냅샷은 수정이 불가능한 객체(immutable)이다.

    하지만, 스냅샷에서 제공하는 map과 asyncMap 메서드로 수정된 새로운 스냅샷을 만들어낼 수 있다.

    map과 asyncMap은 콜백함수를 인자로 받으며, 여기엔 Recoil 상태의 set, reset 함수가 매개변수로 담긴다.

    • map : 스냅샷을 동기적으로 수정할 수 있는 메서드
    • asyncMap : 스냅샷을 비동기적으로 수정할 수 있는 메서드
    class MutableSnapshot {
      set: <T>(RecoilState<T>, T | DefaultValue | (T => T | DefaultValue)) => void;
      reset: <T>(RecoilState<T>) => void;
    }

     

    아래는 map을 사용해서, 특정 시점에 아톰 상태를 수정하는 예시이다. 

    function Example() {
      const updateSnapshot = useRecoilCallback(({ snapshot }) => async () => {
        const newSnapshot = snapshot.map(({ set }) => 
          set(exampleAtom, 100)
        );
        console.log("Changed Value: ", newSnapshot);
      });
    
      return (
        <div>
          <button onClick={updateSnapshot}>스냅샷 변경 및 확인</button>
        </div>
      );
    }

     

     

    - 특정 스냅샷으로 되돌리기

    마지막으로, useGotoRecoilSnapshot() 메서드를 통해 특정 스냅샷으로 이동할 수 있다. 

    예시와 같이, useGotoRecoilSnapshot() 메서드는 인자로 이동할 스냅샷 객체를 넘겨주면 된다.

    function Example() {
      const [snapshotList, setSnapshotList] = useState([]);
      const updateSnapshot = useRecoilCallback(({ snapshot }) => async () => {
        setSnapshotList(prevList => [...prevList, snapshot]);
      });
      const gotoSnapshot = useGotoRecoilSnapshot();
    
      return (
        <div>
          <button onClick={updateSnapshot}>현재 스냅샷 보관</button>
          <ul>
            {snapshotList.map((snapshot, index) => (
              <li key={index}>
                <button onClick={() => gotoSnapshot(snapshot)}>
                  Snapshot #{index + 1} 이동
                </button>
              </li>
            ))}
          </ul>
        </div>
      );
    }

    🤔 마무리 및 Recoil에 대한 생각

    저번 글의 Family에 이어, 스냅샷이라고 하는 Recoil의 기능을 알아보았다.

    아토믹 패턴이 매우 심플에서 맘에도 들었고, React의 기원인 페이스북에서 개발하기에 Recoil을 주로 사용해버릇 해왔다.

     

    하지만, 이전 jotai를 썼을때, 비슷한 아토믹 컨셉이면서도 Recoil에 비해 유용한 기본기능들을 꽤나 제공했다.(store 저장 등)

    또, 트랜드를 보니 zustand유의미한 상승세도 볼 수 있었다.

     

    그간 Recoil에 안주했던 마음을 잠시 접어두고, 상태관리 라이브러리들에 대한 탐구를 조금 진행해봐도 좋겠다는 생각이 들었다!

     

     

    📌 References

    - Recoil 공식문서 : https://recoiljs.org/docs/api-reference/core/useRecoilCallback/

    - 김태곤 님의 블로그 : https://taegon.kim/archives/10126  

    - leirbag 님의 블로그 : https://leirbag.tistory.com/148  

     

    반응형
Designed by Tistory.