230622 React.memo | useCallback| useMemo

나윤빈·2023년 6월 22일
0

TIL

목록 보기
7/55

💡 리-렌더링의 발생 조건

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

➡️ 부모 컴포넌트에서 state가 바뀌었으면, 근데 내려주는 props가 있다면 props로 바뀜

💡 최적화
리액트에서 리렌더링이 자주 일어난다는 것은? cost가 많이 든다, 비싸다! 그렇다면? 최적화(Optimization)이 필요함
불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법이 바로 React.memo, useCallback, useMemo이다

📌 React Hook 최적화 : React.memo

1) 부모 컴포넌트가 리렌더링 되면서 자식 컴포넌트 모두 리렌더링 되는데 이때 자식 컴포넌트의 리렌더링을 막고 싶다면? ✏️ React.memo

2) 형태

React.memo

3) 사용하기

구조

App.jsx

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

function App() {
  console.log("App 컴포넌트가 랜더링 되었습니다!");
  const [count, setCount] = useState(0);

  const onPlusButtonHandler = () => {
    setCount(count + 1);
  };

  const onMinusButtonHandler = () => {
    setCount(count - 1);
  };
  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonHandler}>+</button>
      <button onClick={onMinusButtonHandler}>-</button>
      <div
        style={{
          display: "flex",
          marginTop: "10px",
        }}
      >
        <Box1 />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

export default App;

➡️ 3개의 components (Box1, Box2, Box3) 만들고 App.jsx의 자식 컴포넌트로 넣어주기

결과

➡️ 플러스 버튼이든 마이너스 버튼이든 부모 컴포넌트의 버튼을 누르는 순간 3개의 자식 컴포넌트(Box1, Box2, Box3)가 모두 리렌더링 됨!

📌 React.memo로 해결

React.memo를 통해 컴포넌트를 메모리에 저장해두고 필요할 때 갖다 쓰기(캐싱)
부모 컴포넌트의 state가 변경이 돼서 props의 변경이 일어나지 않는 한, 컴포넌트는 리렌더링이 되지 않는다! (컴포넌트 menoization)

어떻게?

export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);

자식 컴포넌트를 export할 때 React.memo로 감싸준다!

➡️ 플러스 혹은 마이너스 버튼을 누르면 App.jsx만 렌더링 됨!

📌 React Hook 최적화 : useCallback

1) useCallback?

인자로 들어오는 함수 자체를 기억(memoization)

2) 필요성

🤔 만약 Box1이 count를 초기화 해주는 것이라면?

App.jsx

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

function App() {
  console.log("App 컴포넌트가 랜더링 되었습니다!");
  const [count, setCount] = useState(0);

  const onPlusButtonHandler = () => {
    setCount(count + 1);
  };

  const onMinusButtonHandler = () => {
    setCount(count - 1);
  };

  const initCount = () => {
    setCount(0);
  };
  
  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonHandler}>+</button>
      <button onClick={onMinusButtonHandler}>-</button>
      <div
        style={{
          display: "flex",
          marginTop: "10px",
        }}
      >
        <Box1 initCount={initCount} />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

export default App;

➡️ setCount를 초기화 해주는 함수(initCount)를 만들고 Box1에 props로 내려준다

Box1.jsx

import React from "react";

const style = {
  width: "100px",
  height: "100px",
  backgroundColor: "#01c49f",
  color: "white",
};

function Box1({ initCount }) {
  console.log("Box1 컴포넌트가 랜더링 되었습니다!");
  return (
    <div style={style}>
      <button
        onClick={() => {
          initConut();
        }}
      >
        초기화
      </button>
    </div>
  );
}

export default React.memo(Box1);

➡️ Box1컴포넌트에서 initCount를 props로 받아서 버튼을 눌렀을 때 initCount가 호출되도록 함!

🤔 Box1을 React.memo로 memoization해줬음에도 플러스 혹은 마이너스 버튼을 누르면 App.jsx와 Box1.jsx가 리렌더링 됨

왜❓ 함수형 컴포넌트를 사용했기 때문! App.jsx는 컴포넌트이지만 결국은 함수!

initCount는 App 컴포넌트가 리렌더링 될 때 다시 만들어짐 그리고 Box1 안에 있는 onclick도 initCount가 다시 만들어졌기 때문에 props로 새로운 값을 받았다고 생각함

🤔 initCount가 setCount를 0으로 만들어주는 함수라는 것에는 변함이 없는데 리액트는!! Box1이 props로 내려온 initCount가 바뀌었다고 판단했을까

왜❓ 함수도 메모리에 직접 저장되는 것이 아니라, 별도의 공간을 바라보고 있는 주소값을 저장한다. 함수형 컴포넌트가 리렌더링이 되면서 함수가 다시 만들어지면, 이전에 있던 함수는 그대로 있고 initCount를 새로운 공간에 저장하면서 새로운 주소를 다시 return 해준다 결국! initCount는 새로운 주소값을 갖게 된다.

💡 useCallback을 통해 함수 자체를 menoization하는 방법이 필요함

3) useCallback으로 해결하기

🤔 자자 그럼, 플러스 혹은 마이너스 버튼을 누르면 Box1.jsx은 리렌더링 되지 않고 App.jsx만 리랜더링 하도록 해보자!

즉, initCount를 별도 메모리 공간에 저장해놓고 특정 조건이 아닌 경우에는 변경되지 않도록 해보자!

어떻게?

  const initCont = useCallback(() => {
    setCount(0);
  }, []);

➡️ initCount를 useCallback으로 감싸주고, 의존성 배열을 넣어줌 (useCallback도 특정 state가 변경될 때 처음에 저장했던 callback 함수가 갱신되어야 한다면, 의존성 배열에 그 해당 state를 넣어줘야 함)

왜❓ App.jsx이 처음 렌더링 될 때 initCount를 다음과 같이 저장해둠!

  const initCont = () => {
    setCount(0);
  };

따라서 의존성 배열 안에 아무것도 없다? initCount는 변하지 않고 그대로 위의 함수 자체로 기억되고 있음!!!

➡️ 함수가 리렌더랑 되더라도 새롭게 갱신되는 것이 아니라 memoization된 상태로 남아있다. 최초의 주소값으로 남아있다. props 입장에서 다른 props가 내려왔다고 인식할 수 없다.

4) 한 걸음 더...

  const initCont = useCallback(() => {
    console.log(`${count}에서 0으로 변경되었습니다.`);
    setCount(0);
  }, []);

🤔 initCount에 다음과 같이 콘솔을 찍어주면?

왜❓ 5에서 0이 아니라 0에서 0인가요?
➡️ useCallback이 count가 0인 시점에 initCount 함수를 저장하기 때문에

🤔 count가 변경될 때 만큼은 useCallback으로 인해서 initCount가 다시 저장되어야 하지 않을까요

  const initCont = useCallback(() => {
    console.log(`${count}에서 0으로 변경되었습니다.`);
    setCount(0);
  }, [count]);

의존성 배열에 count를 넣어주면?

➡️ 5에서 0으로 변경되었습니다!!

📌 React Hook 최적화 : useMemo

1) React.memo, useCallback, useMemo의 차이

  • React.memo: 컴포넌트 memoization
  • useCallback: 함수 memoization
  • useMemo : value memoization

value? 함수가 return하는 값 혹은 값 자체

2) 형태

const value = useMemo(()=> {
	return 반환할_함수()
}, [dependencyArray]);

useMeno 안에 반환할 함수를 return문으로 써준다
즉, useMeno로 인해 반환할 값을 기억한다
또한 dependencyArray를 통해 특정한 값이 변경될 때 memoization를 다시 수행하게끔 할 수 있다.

3) useMemo 필요성

🤔 heavy Work라는 함수를 만들건데, 이것이 엄청 무거운 컴포넌트라면?

App.jsx

import React from "react";
import HeavyComponent from "./Components/HeavyComponent.jsx";

function App() {
  return (
    <>
      <nav
        style={{
          backgroundColor: "yellow",
          marginBottom: "30px",
        }}
      >
        네비게이션 바
      </nav>
      <HeavyComponent />
      <footer
        style={{
          backgroundColor: "green",
          marginBottom: "30px",
        }}
      >
        푸터 영역이에요!
      </footer>
    </>
  );
}

export default App;

HeavyComponent.jsx

import React from "react";
import { useState } from "react";

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

  const heavywork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };

  const value = heavywork();

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

export default HeavyComponent;

➡️ heavywork가 수행되고 value에 값을 할당해주는 아래 부분은 컴포넌트가 리렌더링 된다면 항상 호출되는 부분
➡️ heavywork는 항상 100을 return하기 때문에 컴포넌트가 리렌더링 될 때마다 heavywork를 수행하는 건 너무 비효율적

4) useMemo 통해 해결하기

import React from "react";
import { useState, useMemo } from "react";

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

  const heavywork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };

  const value = useMemo(() => heavywork(), []);

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

export default HeavyComponent;

➡️ useMemo를 통해 한 번 return된 값을 저장해놓고 변하지 않을 때까지 계속 쓸 수 있음!

5) useMemo의 dependency Array 활용

import React, { useEffect, useState } from "react";

function ObjectComponent() {
  const [isAlive, setIsAlive] = useState(true);
  const [uselessCount, setUselessCount] = useState(0);

  const me = {
    name: "Ted Chang",
    age: 21,
    isAlive: isAlive ? "생존" : "사망",
  };

  useEffect(() => {
    console.log("생존여부가 바뀔 때만 호출해주세요!");
  }, [me]);

  return (
    <>
      <div>
        내 이름은 {me.name}이구, 나이는 {me.age}!
      </div>
      <br />
      <div>
        <button
          onClick={() => {
            setIsAlive(!isAlive);
          }}
        >
          누르면 살았다가 죽었다가 해요
        </button>
        <br />
        생존여부 : {me.isAlive}
      </div>
      <hr />
      필요없는 숫자 영역이에요!
      <br />
      {uselessCount}
      <br />
      <button
        onClick={() => {
          setUselessCount(uselessCount + 1);
        }}
      >
        누르면 숫자가 올라가요
      </button>
    </>
  );
}

export default ObjectComponent;

➡️ useEffect를 me가 바뀌었을 때만 로그가 출력되게끔 했음에도 불구하고 필요없는 숫자 영역에 버튼을 누를 때에도 useEffect부분이 호출 됨

왜❓uselessCount가 바뀌면 해당 컴포넌트가 리렌더링 됨 따라서 컴포넌트 함수가 새로 호출 됨 (me라는 '객체'도 새로운 주소값을 바라봄) 리액트는 me라는 객체의 값이 이전값과 달라졌다고 인식하여 useEffect가 다시 발동 됨

💡 이럴 때 useMemo를 활용하여 me라는 객체의 값이 변하지 않았음을 말할 수 있다!

  const me = useMemo(() => {
    return {
      name: "Ted Chang",
      age: 21,
      isAlive: isAlive ? "생존" : "사망",
    };
  }, [isAlive]);

➡️ 메인 영역의 버튼을 누를 때만 useEffect부분이 호출되고, 필요 없는 숫자 영역에 버튼을 누를 때에는 useEffect부분이 호출 되지 않음
➡️ 필요 없는 숫자 영역에 버튼을 눌러서 컴포넌트가 리렌더링 될 때 useMemo가 최초로 저장해 놨던 me 객체를 다시 메모리에서 꺼내오기 때문에!

🔥 메모리 이슈
useMemo를 남발하게 되면 별도의 메모리 확보를 너무 많이 하게 되기 때문에 오히려 성능이 악화될 수 있음!

profile
프론트엔드 개발자를 꿈꾸는

0개의 댓글