11.30(화) React Hook 다시 정리하기

1일 1TIL 지키지 못한점 반성하면서... 다시 시작해보려고 한다. 이번에는 진짜로 하겠습니다. (회식같은 날을 대비해서 비축분도 미리미리 만들어놔야겠다)

참고) 본 글은 필자의 생각(경험)이 많이 들어간 글이므로 틀릴 수도 있습니다.

React Hook을 다시 정리하는 이유

내가 React를 사용하면서 프로젝트를 진행하다보면 어느정도 복잡해지기 시작하면 이게 왜 2번이나 실행되는거지? 이건 왜 최신화가 안되어있지? 막 이런 상황이 생길때가 간혹있다. 어찌저찌 해결하긴 하지만 이게 맞는건가 싶기도 하고... 오히려 성능이 더 나빠지는거 같아서 다시 정리해보려고 한다.

그리고 내가 useEffect, useCallback, useRef는 자주 사용하는편인데 useMemo, React.memo는 정말 사용하지 않는 편이다. 잘 몰라서 그렇기도 하고 사용할 필요성을 못 느낀거 같기도... 어쨌거나 이번 기회에 다시 한번 정리해보려고 한다.

React Hook

useEffect

useEffect는 정말 기본적으로 많이 사용되는 Hook이라고 본다. 사용법 또한 어렵지 않기 때문에 쉽게 사용할 수 있다. 아래는 예시 코드이다.

  /** Epub init options Changed */
  useEffect(() => {
    if (!url) return;
    
    let mounted: boolean = true;
    let book_: Book | any = null;

    if (!mounted) return;
    
    if (book_) {
      book_.destroy();
    }
    
    book_ = new Book(url, epubFileOptions);
    setBook(book_);

    return () => {
      mounted = false;
    }
  }, [url, epubFileOptions, setBook, setIsLoaded]);


  /** Book Changed */
  useEffect(() => {
    if (!book) return;

    if (bookChanged) bookChanged(book);

    book.loaded.navigation.then(({ toc }) => {
      const toc_: Toc[] = toc.map(t => ({ 
        label: t.label, 
        href: t.href 
      }));
      
      setIsLoaded(true);
      if (tocChanged) tocChanged(toc_);
    });

    book.ready.then(function() {
      if (!book) return;
      
      const stored = localStorage.getItem(book.key() + '-locations');
      if (stored) {
          return book.locations.load(stored);
      } else {
          return book.locations.generate(1024);
      }
    }).then(() => {
      if (!book) return;
      localStorage.setItem(book.key() + '-locations', book.locations.save());
    });
  }, [book, bookChanged, tocChanged]);

useEffect란 deps가 변경되면 리렌더링 한다는 뜻으로 이해하면 될 거 같다. 즉, url, epubFileOptions 같은 내용이 바뀌면 useEffect안에 있는 내용이 실행된다는 뜻이다.

보통 useEffect는 위와 같이 분리시켜서 작성하는 편이 좋은거 같다. 그니까 위에서는 url, epubFileOptions 같은 내용이 바뀌면서 setBook을 통해 book을 저장하고 있다. 이렇게 상태가 바뀐 book은 아래 useEffect가 실행되는 계기가 된다.

useCallback

useCallback 같은 경우는 리렌더링 될때마다 함수가 새로 생성되는 것을 막는 역할을 한다. 아래 예시 코드를 살펴보자.

 const movePage = useCallback((type: "PREV" | "NEXT") => {
    if (!rendition) return;
    if (type === "PREV") rendition.prev();
    else rendition.next();
  }, [rendition]);

  const handleKeyPress = useCallback(({ key }: any) => {
    key && key === "ArrowLeft" && movePage("PREV");
    key && key === "ArrowRight" && movePage("NEXT");
  }, [movePage]);

movePage 같은 함수는 rendition이라는 상태가 변할때만 새로 생성되면서 최신 상태를 받아오게 된다. 만약 useCallback을 안써주면 동일하게 동작은 되겠지만 여러 useState가 있는경우, 다른 state가 변경 될 때도 매번 함수를 새로 생성하므로 불필요한 동작을 하게 되는 것이다.

useRef

useRef는 쓰임새가 2가지가 있다. 하나는 DOM element에 직접 접근해서 조작할때, 또 하나는 특정 값을 기억하고 싶은데 리렌더링은 막고 싶을때.

아래는 첫번째 경우이다. canvas 요소에 직접 접근해서 width, height를 설정할 수 있다.

const canvasRef = useRef(null)

const canvas = canvasRef.current
canvas.height = viewport.height
canvas.width = viewport.width

<canvas id="pdf-render" ref={canvasRef}></canvas>

다음은 두번째 경우이다. timeRef.current에다가 값을 저장하면서도 리렌더링은 되지 않는다.

const timeRef = useRef()

const startTimer = useCallback(() => {
    //console.log('time start')
    interval.current = setInterval(() => {
      timeRef.current += 10
    }, 10)
  }, [])

useMemo

내가 앞에서 말했듯이 useCallback은 알겠는데 useMemo는 잘 모른다고 했었다. 근데 알고보면 쉽게 이해할 수 있다. (참고 : https://crmrelease.tistory.com/41)

다음과 같이 되어있는 경우 countActiveUser는 users가 변화가 있을때만 다시 실행되었으면 좋겠는데, 아무 조치도 하지 않은 경우는 모든 state 변화마다 count가 다시 호출되어 실행된다.

const count = countActiveUsers(users) 
<div>{count}</div>

이러한 경우 아래와 같이 useMemo를 사용하면 좋은데, 이렇게 하면 deps가 변경되기 전까지 값을 기억하고, 실행후 값을 보관하는 용도로 사용한다.

const count = useMemo(() => countActiveUsers(users), [users])

✨ useCallback과 다른점을 알겠는가? useMemo는 복잡한 함수의 return값을 기억하고자 할때, useCallback은 리턴 값이 없는 단순 함수만 실행시키는 경우... 사용하는거 같다.

React.memo

참고 : https://ui.toast.com/weekly-pick/ko_20190731

사실 제일 까다로운 녀석은 이녀석이 아닐까 생각한다. 한마디로 설명하자면 props가 같다면 상위 컴포넌트가 리렌더링 되더라도 하위 컴포넌트는 리렌더링 되는 것을 막는 역할을 한다고 한다.

즉, 아래와 같은 경우 Movie에 아무 조치를 하지 않았다면 MovieViewsRealtime에 props가 바뀔때마다 하위 Movie 컴포넌트도 리렌더링 된다.

function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <Movie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}
// Initial render
<MovieViewsRealtime
  views={0}
  title="Forrest Gump"
  releaseDate="June 23, 1994"
/>

// After 1 second, views is 10
<MovieViewsRealtime
  views={10}
  title="Forrest Gump"
  releaseDate="June 23, 1994"
/>

만약 Movie에다가 React.memo를 사용했다면, title 혹은 releaseDate props가 같다면, React는 Movie를 리렌더링 하지 않을 것이다. memo라는 뜻이 메모이제이션(Memoization)이라는 뜻이기 때문에.

한가지 참고할만한 점은 같다 다르다를 판단할때 Shallow 비교를 한다고 한다. 그렇기 때문에 primitive type이 아닌 객체,배열,함수와 같은 reference type은 같은 참조가 아니라면 새로운 값으로 판단하게 된다.

예컨데 같은 값의 props라도 컴포넌트의 state가 변경되면 객체, 배열, 함수와 같은 reference type은 새로 생성되어버리니까 다르게 판단해버린다는 뜻이다. 그렇기 때문에 useCallback, useMemo를 통해 특정 props에 dependency를 걸어줌으로써 렌더링 횟수를 줄일 수 있다.

좋은 웹페이지 즐겨찾기