• [use-memo-one] useMemo/useCallback 개선 :: 마이구미
    알아두면 좋은 라이브러리 2021. 11. 29. 21:33
    반응형
    이 글은 useMemo/useCallback 와 관련된 라이브러리 use-memo-one 을 다룬다.
    README.md 중심의 내용과 실제 코드를 통해 리뷰해본다.
    조금이나마 도움을 줄 수 있는 라이브러리들을 소개하는 카테고리로 분류된 글이다.
    알아두면 좋은 라이브러리

    참고로 use-memo-one 은 이전 글에서 다뤘던 memoize-one 라이브러리의 제작자이다.

    README.md 에서 use-memo-one 를 표현하는 문장을 보자.

     

    useMemo and useCallback with a stable cache (semantic guarantee)

     

    지금 당장은 stable cahce, semantic guarantee 용어는 쉽게 이해하기는 힘들 것이다.

    우선 쉽게 말하자면, React 에서 제공하고 있는 useMemo 와 useCallback 이 가지고 있는 이슈를 개선한 라이브러리이다.

     

    캐시?, 시맨틱 보증? 에 앞서 useMemo/useCallback 을 간단히 살펴보자.

    useMemo/useCallback 은 React 에서 많은 리랜더링으로 인해 발생하는 성능 문제를 개선하기 위해 사용한다.

     

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

     

    두번째 인자인 의존성에 설정된 값(a, b)이 변하지 않으면, 다음 리랜더링에서는 재계산하지 않고 기억된 결과를 그대로 사용한다.

     

    사실 눈에 보일만큼 크게 성능이 개선되는 건 아니더라도, 유용한 기능이다.

    그렇다면, 이것이 무슨 문제가 있길래 use-memo-one 라이브러리가 탄생하였는가?

    React 공식 문서의 설명을 확인해보자.

     

    You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

     

    중요한 문장은 "useMemo 는 성능 최적화에 의존하고 의미론적인 보증(semantic guarantee)은 하지 않는다" 라는 것이다.

    의미하는 바를 자세히 알아보자.

    React 는 useMemo 의 의존성이 변하는 시점에 선언된 콜백 함수를 실행하여 재계산을 한다고 했다.

    하지만 의미론적인 보증을 하지 않는다는 것은, 의존성이 변하지 않는 시점에서도 재계산을 할 수 있다는 의미가 된다.

    React 에서는 의존성 변화 유무에 상관없이 메모리 확보를 위해 다음 랜더링에서 재계산할 수도 있다고 언급하고 있다.

    요약하면 선언된 의존성의 변화가 없으면 기억된 결과를 사용하겠지만, "이것이 100% 보장은 아니다" 라고 해석할 수 있다.

     

    그렇다면, 실제로 발생할 수 있는 이슈는 무엇인가?

     

    const { keyword } = props;
    const keywordList = useMemo(() => keyword.split(' '), [keyword]);

     

     

    keywordList 는 문장을 나타내는 Props 의 keyword 값을 useMemo() 를 활용해서 문장을 단어별로 배열로 분류한 값이다.

    그리고 배열로 분류된 단어들을 뒤집는 함수가 필요하면 다음과 같다.

     

    const flipList = useCallback(() => { ... }, [keywordList]);

     

    만약 keyword 가 변하지 않았음에도, 메모리 확보를 위해 useMemo 가 재계산되면 어떻게 되는가?

    flipList 는 keywordList 재계산에 의해 다시 호출되겠지만, 전혀 문제될 것이 없다.

     

    만약 useEffect 에 의존성에 keywordList 추가하면 어떻게 될까?

    keywordList 의 변화에 따라 api 요청한다고 가정해본다.

     

    useEffect(() => { api.send('', keywordList) }, [keywordList]);

     

    그리고 어떤 시점에 keywordList 가 변경이 안되었지만, 메모리 확보를 위해 useMemo 는 재계산한다고 가정해보자.

    그러면 useEffect 의 의존성인 keywordList 의 변화로 인해, 콜백 함수가 호출되서 api 를 요청하게 된다.

    filpList 는 이러한 이슈에 민감하지 않았지만, useEffect 는 보다시피 이슈에 민감하다.

    결과적으로 이러한 상황을 인지하지않으면, 예기치 못한 상황을 초래할 수 있다.

     


     

    memoize-one 과 비슷한 원리로 동작하고, 간단한 코드로 50 줄도 안된다.

    두번째 전달인자인 의존성의 변화 유무에 따라 Cache 여부를 결정한다.

     

    const isFirstRun = useRef<boolean>(true);
    const committed = useRef<Cache<T>>(initial);
      
    const useCache: boolean =
        isFirstRun.current ||
        Boolean(
          inputs &&
            committed.current.inputs &&
            areInputsEqual(inputs, committed.current.inputs),
        );

     

    • isFirstRun - 첫 로드 시점에는 당연히 캐시된 데이터가 없기 때문에 useCache 값을 false 로 만들기 위함.
    • commited - 캐시 유무를 결정하는 의존성을 나타내는 전달인자를 저장해놓는 용도.
    • useCache - 캐시 유무

     

    대부분 useRef 는 HTML 요소에 접근하는 용도로 사용하지만, 위처럼 변수처럼 사용할 수 있다.

    state 로 사용하면 랜더링과 밀접한 관계가 있어, 전역 변수 형태로 사용하기가 곤란하다.

    이러한 상황에서는 랜더링과 영향이 없는 useRef 의 current 를 활용할 수 있다.

     


     

    이처럼 use-memo-one 은 캐시에 대해 100% 보장을 해주는 라이브러리이다.

    하지만 보장을 위해서는 당연히 useMemo 보다는 메모리를 더 소비할 것이다.

    그래도 useMemo 는 React 에 의해 캐시가 정리되지만, useMemoOne 은 React 가 아닌 GC 에 의해 정리된다.

    이것이 의미론적으로는 조금 더 맞는 그림이지 않을까? 하는 개인적인 생각이다.

     

    제작자가 React 에 직접 issue 올려서 토론한 내용이다.

    한번 보면 많은 도움이 될 것이다.

    https://github.com/facebook/react/issues/15278

     

    반응형

    댓글 0

Designed by Tistory.