[TIL] React.memo / useCallback / useMemo

·2023년 11월 9일
1

TIL

목록 보기
27/85
post-thumbnail

최적화(Optimizaion)

Rerendering이 발생하는 조건
1. 컴포넌트에서 state가 바뀌었을 때
2. 컴포넌트가 내려받은 props가 변경되었을 때
3. 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두

리액트에서 리렌더링이 빈번하게, 자주 일어난다는 것은 비용이 발생한다는 것이므로 최적화(Optimizaion)를 통해 불필요한 리렌더링이 발생하지 않도록 해야 한다.

⭐️최적화하는 대표적인 방법⭐️

  • memo(React.memo) : 컴포넌트를 캐싱
  • useCallback : 함수를 캐싱
  • useMemo : 값을 캐싱 (함수가 리턴하는 값)

✅ React.memo

memo 란?

👉 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트가 리렌더링되는 것을 건너뛸 수 있다.
👉 props가 변경되지 않은 경우 이 컴포넌트를 리렌더링하는 것을 건너뛸 수 있다.

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

예시 1️⃣

// App.jsx
import React, { useState } from "react";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";

const boxesStyle = {
  display: "flex",
  marginTop: "10px",
};

function App() {
  console.log("App 컴포넌트가 렌더링되었습니다!");

  const [count, setCount] = useState(0);

  // 1을 증가시키는 함수
  const onPlusButtonClickHandler = () => {
    setCount(count + 1);
  };

  // 1을 감소시키는 함수
  const onMinusButtonClickHandler = () => {
    setCount(count - 1);
  };

  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div style={boxesStyle}>
        <Box1 />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

export default App;

Box 컴포넌트에는 각각 "Box1이 렌더링 되었습니다." 를 콘솔에 출력하는 코드가 있다.
+버튼, -버튼을 클릭할 때 count라는 state가 변경되어 부모 컴포넌트 (App)가 리렌더링 되고, 자식 컴포넌트(Box1, Box2, Box3) 도 같이 리렌더링 된다.
(콘솔에 아래와 같은 로그가 반복적으로 찍힌다.)

그러나 자식 컴포넌트(Box1, Box2, Box3) 입장에서는 바뀐 것이 없는데 리렌더링 되는 것이 불필요하다. 그러므로 React.memo를 통해 불필요한 리렌더링을 막을 수 있다.

// 변경 전
export default Box1; // Box1.jsx
export default Box2; // Box2.jsx
export default Box3; // Box3.jsx
// 변경 후
export default React.memo(Box1); // Box1.jsx
export default React.memo(Box2); // Box2.jsx
export default React.memo(Box3); // Box3.jsx

이렇게 하면 첫 렌더링 이외에는 +버튼, -버튼을 클릭하여 App.jsx의 state가 변경되더라도 자식 컴포넌트인 Box1, Box2, Box3 은 리렌더링이 되지 않는다.
(콘솔에 "App 컴포넌트가 렌더링되었습니다!" 만 반복적으로 찍힌다.)

예시 2️⃣

function Btn({ text, changeValue }) {
  console.log(text, "was rendered");
  return (
    <button
      onClick={changeValue}
      style={{
        backgroundColor: "tomato",
        color: "white",
        padding: "10px 20px",
        border: 0,
        borderRadius: 10,
      }}
    >
      {text}
    </button>
  );
}

function App() {
  const [value, setValue] = useState("Save Changes");
  const changeValue = () => setValue("Revert Changes");
  return (
    <div>
      <Btn text={value} changeValue={changeValue} />
      <Btn text="Continue" />
    </div>
  );
}

첫 번째 Btn은 리렌더링 되어야 한다. setValue가 실행되어서 props가 변경되었으니까. 하지만 두 번째 Btn은 변경 사항이 없어서 리렌더링 할 필요가 없다.
그러나 부모 컴포넌트 (App) 에서 state를 변경하고 있으므로 첫 번째 Btn 두 번째 Btn 모두 리렌더링 된다.
이 때 우리는 React.memo 를 통해 만약 props 가 변경되지 않는다면 이 컴포넌트가 리렌더링 하는 것을 막을 수 있다.
const MemorizedBtn = React.memo(Btn); 이렇게!

...
const MemorizedBtn = React.memo(Btn); // 메모
function App() {
  const [value, setValue] = React.useState("Save Changes");
  const changeValue = () => setValue("Revert Changes");
  return (
    <div>
      <MemorizedBtn text={value} changeValue={changeValue} />
      <MemorizedBtn text="Continue" />
    </div>
  );
}

이제 첫 번째 Btn을 클릭해도 두 번째 Btn이 리렌더링 되지 않는다!

✅ useCallback

useCallback 이란?

👉 useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있는 React Hook이다.

const cachedFn = useCallback(fn, [의존성 배열])

예시

// App.jsx
...
  // count를 초기화해주는 함수
  const initCount = () => {
    setCount(0);
  };

  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div style={boxesStyle}>
        <Box1 initCount={initCount} />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

...
...
// Box1.jsx
function Box1({ initCount }) {
  console.log("Box1이 렌더링되었습니다.");
  return (
    <div style={boxStyle}>
      <button onClick={initCount}>초기화</button>
    </div>
  );
}

export default React.memo(Box1);

+버튼, -버튼, 초기화버튼을 누를 때 모두 위와 같은 로그가 콘솔에 찍힌다.
React.memo를 통해 Box1을 메모이제이션 했음에도 불구하고 Box1이 리렌더링 된다.
그 이유는 App 컴포넌트가 리렌더링 될 때 initCount 함수도 다시 저장하게 되는데, 이 때 내용물은 같더라도 다시 만들어지면 그 주솟값이 달라지게 되어 자식 컴포넌트인 Box1은 props인 initCount가 변경되었다고 인식하는 것이다.
(함수도 객체의 한 종류이므로 주솟값만 저장 -> 불변성 part 참고!)
이 때 initCount 함수를 캐싱하기 위해 useCallback Hook을 사용하면 된다.

// 변경 전
const initCount = () => {
  setCount(0);
};

// 변경 후
const initCount = useCallback(() => {
  setCount(0);
}, []);

이렇게 하면 App 컴포넌트가 리렌더링 되어도 Box1 컴포넌트는 리렌더링 되지 않는다.

✅ useMemo

useMemo 란?

👉 useMemo는 리렌더링 사이에 계산 결과를 캐시할 수 있는 React Hook이다.
👉 함수가 리턴하는 값을 캐싱한다.

const cachedValue = useMemo(calculateValue, [의존성 배열])

동일한 값을 반환하는 함수를 계속 호출할 때 생기는 불필요한 렌더링을 줄이기 위해 함수가 맨 처음 값을 반환할 때 그 값을 메모리에 저장한다.

이렇게 하면 매번 함수를 호출해서 계산하는 것이 아니라 필요할 때마다 이미 저장한 값을 꺼내와서 쓸 수 있다. 이를 캐싱 이라고 표현한다.

예시

// App.jsx
import "./App.css";
import HeavyComponent from "./components/HeavyComponent";

function App() {
  const navStyleObj = {
    backgroundColor: "yellow",
    marginBottom: "30px",
  };

  const footerStyleObj = {
    backgroundColor: "green",
    marginTop: "30px",
  };

  return (
    <>
      <nav style={navStyleObj}>네비게이션 바</nav>
      <HeavyComponent />
      <footer style={footerStyleObj}>푸터 영역이에요</footer>
    </>
  );
}

export default App;
// HeavyComponent.jsx
import React, { useState, useMemo } from "react";

function HeavyComponent() {
  const [count, setCount] = useState(0);

  const heavyWork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100; // 무거운 작업
  };

  // CASE 1 : useMemo를 사용하지 않았을 때
  // const value = heavyWork();

  // CASE 2 : useMemo를 사용했을 때
  const value = useMemo(() => heavyWork(), []);

  return (
    <>
      <p>나는 {value}을 가져오는 엄청 무거운 작업을 하는 컴포넌트야!</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        누르면 아래 count가 올라가요!
      </button>
      <br />
      {count}
    </>
  );
}

export default HeavyComponent;

HeavyComponent 안에서는 const value = heavyWork()를 통해 value 를 세팅하고 있다. count라는 state가 변경될 때마다 heavyComponent가 리렌더링되어 heavyWork라는 함수도 계속해서 호출이 된다. 그리고 heavyWork() 가 매우 무거운 작업이라 함수 호출할 때마다 성능이 저하된다. 하지만 useMemo() 로 감싸주게 되면 리렌더링 할 때마다 함수를 다시 호출하지 않는다.
const value = useMemo(() => heavyWork(), []);
=> 성능이 향상된다!

주의사항

useMemo를 남발하게 되면, 별도의 메모리를 너무 많이 확보하게 되어 오히려 성능이 악화될 수 있다. 🙂

profile
느리더라도 조금씩, 꾸준히

0개의 댓글