관리 메뉴

공부기록용

React숙련주차07(React Hooks 최적화 - React.memo/useCallback/useMemo💫) 본문

📚강의록📚/스파르타)React

React숙련주차07(React Hooks 최적화 - React.memo/useCallback/useMemo💫)

과부하가즈아 2023. 6. 30. 16:55

🔴React.memo_component

🔴useCallback_함수

🔴useMemo_value


💫리-렌더링의 발생 조건
🔻컴포넌트에서 state가 바뀌었을 때
🔻컴포넌트가 내려받은 props가 변경되었을 때
🔻부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두

💫리액트에서 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법
🔻memo(React.memo) : 컴포넌트를 캐싱
🔻useCallback : 함수를 캐싱
🔻useMemo : 값을 캐싱
// App.jsx
import React, { useState } from 'react'
import Box1 from './components/Box1';
import Box2 from './components/Box2';
import Box3 from './components/Box3';

function App() {

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

  const plusCountHandler = function () {
    return setCount(count + 1)
  }

  const minusCountHandler = function () {
    return setCount(count - 1)
  }

  return (
    <>
      <h3>카운트 예제입니다</h3>
      <p>현재 카운트 : {count} </p>
      <button onClick={plusCountHandler}>+</button>
      <button onClick={minusCountHandler}>-</button>
      <div style={{
        display: "flex",
        marginTop: "10px"
      }}>
        <Box1 />
        <Box2 />
        <Box3 />
      </div>

    </>

  )
}

export default App
// Box1.jsx
import React from 'react'

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


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

export default Box1
// Box2.jsx
import React from 'react'

const boxStyle = {
    width: "100px",
    height: "100px",
    backgroundColor: "#4e93ed",
    color: "white",
  };


function Box2() {
    console.log("Box2이 렌더링되었습니다.");
    return <div style={boxStyle}>Box1</div>;
}

export default Box2
// Box3.jsx
import React from 'react'

const boxStyle = {
    width: "100px",
    height: "100px",
    backgroundColor: "#4e93ed",
    color: "white",
  };


function Box2() {
    console.log("Box2이 렌더링되었습니다.");
    return <div style={boxStyle}>Box1</div>;
}

export default Box2

> plus 버튼 또는 minus 버튼을 누른 순간 Box1,2,3의 컴포넌트도 다같이 리렌더링이 되는 걸 알 수 있고, 

> 이는 React.memo를 이용해서 컴포넌트를 메모리에 저장해두고 필요할 때 갖다 쓰게 된다.

> 이렇게 하면 부모 컴포넌트의 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않으며 이것을 컴포넌트 memoization 이라고 한다.

memo는 memoization을 뜻한다.

동일한 값을 반환하는 함수를 계속 호출해야 하면 불필요한 렌더링을 한다고 볼 수 있다. 맨 처음 해당 값을 반환할 때 그 값을 특별한 곳(메모리)에 저장하면 필요할 때 마다 다시 함수를 호출해서 계산하는게 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있다. 보통 이러한 기법을 캐싱을 한다고 표현하기도 한다.

memo(React.memo)

컴포넌트를 memoization

// App.jsx
// memo import

// 1.
import { memo } from "react";

// 2.
React.Memo

+

Box1,2,3 각각의 export 부분을 다음과 같이 변경해주면 된다

// Box1.jsx
export default React.memo(Box1);

// Box2.jsx
export default React.memo(Box2);

// Box3.jsx
export default React.memo(Box3);

그러면 최초 렌더링 이외에는 App.jsx 컴포넌트의 state가 변경되더라도 자식 컴포넌트들은 렌더링이 되지 않는다. App.jsx 컴포넌트만 렌더링이 됬다.


Box1에 초기화 버튼을 만들어서 카운트한 숫자를 리셋시키는 기능을 부여했다. 근데 Box1에 메모리제이션으로 리렌더링을 막도록했는데 그 기능을 하게 되면 Box1이 다시 리렌더링 된다

// App.jsx

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

function App() {

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

  const plusCountHandler = function () {
    return setCount(count + 1)
  };

  const minusCountHandler = function () {
    return setCount(count - 1)
  };


  // 함수형 컴포넌트로 
  // 주소를 부여하여 별도의 공간에 저장함
  // App.jsx가 리렌더링 되면서 다시 만들어지면 그 주솟값이 달라지고
  // 하위 컴포넌트인 Box1.jsx는 props가 변경됐다고 인식
  const initCount = function () {
    return setCount(0);
  };


  return (
    <>
      <h3>카운트 예제입니다</h3>
      <p>현재 카운트 : {count} </p>
      <button onClick={plusCountHandler}>+</button>
      <button onClick={minusCountHandler}>-</button>
      <div style={{
        display: "flex",
        marginTop: "10px"
      }}>
        <Box1 initCount={initCount}/>
        <Box2 />
        <Box3 />
      </div>

    </>

  )
}

export default App
// Box1.jsx
import React from 'react'

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


function Box1({ initCount }) {       // 왜 initCount가 바뀌었다고 인식할까 -> 왜 Box1이 리렌더링 되는 걸까
    console.log("Box1이 렌더링되었습니다.");
    return <div style={boxStyle}>
        <button
            onClick={function () {
                return initCount();  // onclick가 일어나면 함수 발동 -> return으로 initCount발동
            }}>
            초기화
        </button>
    </div>;
}

export default React.memo(Box1);

> 내가 이해하기로는 전체적으로 리렌더링 되면서 메모제이션 이전에는 Box1,2,3이 같이 리렌더링 됬는데 이게 비효율적이니, 메모이제이션을 통해 Box1,2,3에 각각 React.memo를 사용해서 리렌더링을 막았다. 

> 하지만 초기화에 함수를 걸어주면서 실행하게 했고 이것 또한 값으로는 변경되는 것이 없다고 해도 함수 자체로써는 기능을 하면서 자체의 주소값이 바뀌게 되고 이것으로 인해 리렌더링이 일어난다고 이해하면 될 것 같다. 

> 그래서 이걸 담당하고 있는 Box1의 측면에서는 props가 변경됬다고 인식해서 리렌더링을 다시하는 것으로 생각하자

> 그래서 이 직접적인 값이 변하지 않은 Box1의 리렌더링을 막아주기 위해서 이 함수 자체를 memoization하는 방법으로 useCallback 사용하는 것이다. 

> initCount라는 함수를 별도의 메모리 공간에 저장을 해두고 특정 조건이 아닌 경우에는 아예 변경되지 않도록 막는게 중요하다. 


useCallback

React.memo는 컴포넌트를 memoization했다면, useCallback은 인자로 들어오는 함수 자체를 기억(memoization)한다.

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

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

> useCallback로 감싸주고, 의존성 배열을 마지막에 넣어주기!

 

> App.jsx가 처음 렌더링이 될 때, iniCount함수를 메모리 공간에 그대로 저장함(setCount(0)으로 저장되고 있는것)

> 그래서 함수가 리렌더링 되더라도 새롭게 갱신되는게 아니라 memoization되어있는 상태로 남아있다.

> 그래서 처음 App.jsx에서 만든 최초의 주소값으로 계속 props 받고 있는 것으로 하위 컴포넌트의 입장에서는 다른 props가 받아졌다고 인식할 수가 없는것이다. 

 

> App.jsx가 처음 렌더링이 될 때, iniCount함수를 메모리 공간에 그대로 저장한다는 의미는 iniCount함수의 (초기화이므로) count가 0이 되는 시점을 저장했다는 의미이다. 

>> 그래서 [의존성배열]에 count를 넣어줌으로써 기존 코드의 count가 변경 될 때 마다 새롭게 함수를 할당하게 된다. 


useMemo

value를 memoization한다.

memo는 memoization을 뜻한다.

동일한 값을 반환하는 함수를 계속 호출해야 하면 불필요한 렌더링을 한다고 볼 수 있다. 맨 처음 해당 값을 반환할 때 그 값을 특별한 곳(메모리)에 저장하면 필요할 때 마다 다시 함수를 호출해서 계산하는게 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있다. 보통 이러한 기법을 캐싱을 한다고 표현하기도 한다.
const value = useMemo(function() {
	return 반환할_함수()
}, [dependencyArray]);

dependency Array의 값이 변경 될 때만 반환할_함수()가 호출된다.


// App.jsx
import React from 'react'
import HeavyComponent from './components/HeavyComponent'

// heavy work -> 엄청 무거운 작업이라고 가정
function App() {
  return (
    <>
      <nav style={{
        backgroundColor:"yellow",
        marginBottom: "30px",
      }}>네비게이션 바</nav>
      <HeavyComponent/>
      <footer style={{
        backgroundColor:"green",
        marginBottom: "30px",
      }}>footer영역이에요</footer>
    </>
  )
}

export default App
// HeavyComponent.jsx
import React from 'react'
import { useState } from 'react';


function HeavyComponent() {

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

    // heavy work -> 엄청 무거운 작업
    // onClick가 실행되면 무조건 돌아가는데 
    // 즉 함수가 호출되면 항상 리렌더링이 된다 -> 이 컴포넌트가 함수 호출될떄마다 실행되는 것은 너무나도 비효율적이다.
    // 그래서 항상 return되는 값을 미리 저장해두면 불필요한 리렌더링이 없을것
    // 이때 값을 저장하는 uesMemo를 사용하는 것이다
    const heavyWork = () => {
        for (let i = 0; i < 100000000; i++) { }
        return 100;
    };

    const value = heavyWork();

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

export default HeavyComponent
const heavyWork = () => {
    for (let i = 0; i < 100000000; i++) { }
    return 100;
};

const value = heavyWork();

> heavy work -> 엄청 무거운 작업

>>onClick가 실행되면 무조건 돌아가는데

> 함수가 호출되면 항상 리렌더링이 된다

>> 이 컴포넌트가 함수 호출될떄마다 실행되는 것은 너무나도 비효율적이다.

> 그래서 항상 return되는 값을 미리 저장해두면 불필요한 리렌더링이 없을것

>> 이때 값을 저장하는 uesMemo를 사용하는 것이다.

const value = useMemo(function(){
        heavyWork()}, []);

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

function ObjectComponent() {
  const [isAlive, setIsAlive] = useState(true); // 기본이 생존-ture
  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;

> 숫자가 올라간다는 버튼을 클릭하면 숫자만 올라가고 생존여부와는 관계가 없는데 생존여부에 따른 console이 같이 실행되는 것으로 보여지는데 ([me]로 의존성배열을 넣어줌에도 불구하고)

 

>버튼이 선택돼서 uselessCount state가 바뀌게 되면

→ ObjectComponent가 리렌더링이 된다. → 컴포넌트 함수가 새로 호출된다.

→ me 객체도 다시 할당한다.(이 때, 다른 메모리 주소값을 할당받게 되는 것)

→ useEffect의 dependency array에 의해 me 객체가바뀌었는지 확인해봐야 하는데

오잉?! 이전 것과 모양은 같은데 주소가 달라요!

→ 처음 me객체의 주소 값과 다시 버튼이 클릭되면서 함수가 실행되어 생기게된 me객체의 주소가 다르다는 얘기로 ObjectComponent자체에서는 값이 달라졌다고 인식하게 되는 것이다. 그래서 useEffect가 작동하게 되는 것이다. 

리액트 입장에서는 me가 바뀌었구나 인식하고 useEffect 내부 로직이 호출됩니다.

 

그래서 이를 막기 위해서 uesMemo를 사용해서 처음의 me객체를 저장해주어야 한다

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

[isAlive] : 생존 사망의 값이 바뀔때만 리렌더링해서 화면에 나타나야하니까 그걸 변경하는 값인 isAlive를 넣어준다. 

Comments