본문 바로가기
WEB/깊게 공부하기

[React] Optimization, useMemo, useCallback, memo - (2)

by IT황구 2022. 3. 9.
728x90
반응형

반갑습니다 ^^ 

2탄을 이어서 작성해 보겠습니다.

 

React.memo는 컴포넌트를 넣으면 컴포넌트를 반환해주는 함수인데요. (HOC)

이것들에 대한 설명을 찾아보면, memo는 props를 비교하고 얕은 비교를 한다. 이런 설명이 전부입니다.

 

이렇게만 알고 가면, 이 케이스에는 렌더링이 일어날까? 이런 것들이 대한 답이 되지 않았습니다.

제가 직접 찾아보고 나니 이제는 대답을 할 수 있을것 같더라구요.

 

memo에 props를 50개를 넣었지만, 실제로 호출시에 3개만 넘겨준다면, 그 3개만 비교하게 될 것입니다.

 

React.memo

이 HOC는 주어진 props를 비교하지만 좀 더 자세히 비교를 합니다. 그리고 시간이 꽤나 걸릴것 같은 작업입니다.

areHookEquals보다 가격이 훨씬 비싸답니다. 

구조는 이렇게 생겼습니다.

  • deps를 전부 비교하던 areHookEquals + Object 같은것들은 {key,value}가 모두 같은지 확인을 하는 작업이 있습니다.
  • Object의 모든 key를 뽑아서 Object[key] === Object[key2] 이런 과정이 있는데.. 같이 보면 됩니다.
  • type은 _ref로 내가 쓴 컴포넌트를 기억하고 있습니다.
    • 그런데 이게 명확하게 스트링을 비교 하려고 갖고있는건지 어디에 쓰는지는 잘 이해를 못했었습니다.
    • 이 부분은 좀 더 리액트 내부 코드를 보는게 익숙해지면 더 보도록 하겠습니다
  • compare은 별다른걸 주지 않으면 null이 들어가 있는데, 나중에 보면 알지만 null은 기본 shallowEqual로 변합니다

updateMemoComponent 소스코드 일부

shallowEqual 내부

- 비교함수를 주지 않으면, shallowEqual이 들어가게 됩니다.

  • shallowEqual은 areHookEquals + object 비교가 존재합니다.
  • 따라서 {a:3, b:2} 와 {b:2, a:3} 을 같게 봅니다. 만약 JSON.stringify(obj)로 비교했다면 다르다고 나왔지만, 이렇게 key로 보니까 같게 나옵니다.

그리고 여기서 만들어진 컴포넌트는 Hook 이라는곳에 Obj 형태로 global하게 저장되고 있습니다.

따라서 memo류를 사용하면 무거운 Object가 하나씩 추가된다고 볼 수 있습니다.

 

언제 써야 할까?

컴포넌트를 생성하는 작업이 비싸면서, 다른 input에 의해 re-render가 자주 일어나는 경우에 좋습니다.

import "./App.css";
import React, { useState, useEffect, memo } from "react";
import axios from "axios";

const BaseCard = memo(({ list }) => {
  return list.map((elem, idx) => <div key={idx}>{elem.title}</div>);
});

// const BaseCard = ({ list }) => {
//   return list.map((elem, idx) => <div key={idx}>{elem.title}</div>);
// };

function App() {
  const [page, setPage] = useState(1);
  const [list, setList] = useState([]);
  const [text, setText] = useState("");

  // const MC = React.memo(MemoizedCard);

  const handleChange = (e) => {
    setText(e.target.value);
  };

  useEffect(async () => {
    const { data } = await axios.get(
      `https://jsonplaceholder.typicode.com/comments/${page}/todos`
    );
    setList([...list, ...data]);
  }, [page]);

  return (
    <div className="App">
      <input type="text" value={text} onChange={handleChange} />
      <BaseCard list={list} />
      <button onClick={() => setPage(page + 1)}>ClickMe</button>
    </div>
  );
}

export default App;

memo가 된것과, 되지 않은경우에 렌더링 속도에 차이를 느낄 수 있습니다. 실제로 profiler로 찍어봐도 속도차이가 많이 나는걸 볼 수 있습니다.

 

 

useCallback

- 렌더링시에 hook을 호출하고, deps를 확인해서 변하지 않았다면 기존의 함수를 반환해 줍니다. (참조가 같습니다)

이건 React.memo와도 많은 관련이 있습니다. (deps 확인은 areHookEquals로 합니다)

 

useCallback에서 props에 function을 넘길경우, 무조건 re-render 및 불필요한 비교를 하게 됩니다.

 

사진에서 알 수 있듯, 생긴건 같아도 두 함수의 참조는 다릅니다. 따라서 Object.is로 비교시 false입니다.

하지만 let c = func로 넘겨준다면, 서로 같은 주소를 가리키므로 참이 됩니다.

 

이걸 이용해서 memo에서 re-render를 막을 수 있습니다.

import "./App.css";
import React, { useState, useEffect, useCallback, memo } from "react";
import axios from "axios";
import Card from "./Card";

const MemoizedCard = memo(({ list, handleTest }) => {
  handleTest();
  return list.map((elem, idx) => <div key={idx}>{elem.title}</div>);
});

function App() {
  const [page, setPage] = useState(1);
  const [list, setList] = useState([]);
  const [text, setText] = useState("");

  const handleChange = (e) => {
    setText(e.target.value);
  };

  const handleTest = useCallback(() => {
    console.log("aa");
  }, []);
// 이것 또한 props로 넘기는 것이 아니라면 불필요한 과정이다.

  // const handleTest = () => {
  //   console.log("aa");
  // };

  useEffect(async () => {
    const { data } = await axios.get(
      `https://jsonplaceholder.typicode.com/comments/${page}/todos`
    );
    setList([...list, ...data]);
  }, [page]);

  return (
    <div className="App">
      <input type="text" value={text} onChange={handleChange} />

      <MemoizedCard list={list} handleTest={handleTest} />
      <button onClick={() => setPage(page + 1)}>ClickMe</button>
    </div>
  );
}

export default App;

// Callback을 제거하고 해보면 체감이 된다.
// handleTest={()=>("aa")} 또한 memoization을 깨는 요인이 된다.

useCallback을 사용한것과, 사용하지 않은경우에 차이를 느껴보면, 참조가 달라서 re-render가 많이 일어나는걸 볼 수 있을 것 입니다.

 

또한 onClick 같은곳에서 내부에서 함수를 선언하면, 매 렌더링마다 새로 생성하므로 좋지 않은 습관이 됩니다.

 

 

결론

  • 오래 걸리는 컴포넌트 or 함수들에 사용하자
  • dependency를 비교하는것 또한 시간이 걸린다. 하지만 shallowEqual은 더 걸린다.
  • 자주 변경되는 값들은 그냥 놔두는것이 좋다. 오히려 props를 비교하는 cost가 더 들게 된다.

지금 사진에 있는 예시는 useEffect에 deps를 많이 달아서 re-render를 막아주는 방식을 해본 예시입니다.

- re-render 방지 전

- re-render 방지 이후

결론적으로 확인하면, rendering 시간이 줄어들고, 실제 2600ms -> 2000ms 가 된걸 볼 수 있습니다.

 

하지만 실제 결과는 한번의 click시에 드는 시간은 오히려 방지 이후에 더 시간이 걸리게 되었습니다.

WHY? 불필요한 render를 막기 위해 useEffect에 deps가 많아서 오히려 시간이 더 들게 된 것이었습니다.

 

React 자체도 이미 충분히 rendering 최적화가 잘 되어있다.

 

이상입니다.

728x90
반응형