ReactJS

React - ( React Hooks 5 - memoization )

jjangsh 2024. 9. 10. 01:58

01. memoization의 개념

 

(1) 리-렌더링의 발생 조건

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

(2) 최적화

리액트에서 리렌더링이 빈번하게, 자주 일어난다는 것은 그렇게 좋은 소식은 아니에요. 비용이 발생하는 것은 최대한 줄여야 합니다. 이런 작업을 우리는 최적화(Optimization)이라고 불러요. 리액트에서 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법이 바로

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

 

02. memo(React.memo)

(1) memo란?

리-렌더링의 발생 조건 중 3번째 경우. 즉, 부모 컴포넌트가 리렌더링 되면 자식컴포넌트는 모두 리렌더링 된다는 것은 그림으로 보면

 

  • 1번 컴포넌트가 리렌더링 된 경우, 2~7번이 모두 리렌더링 된다.
  • 4번 컴포넌트가 리렌더링 된 경우, 6, 7번이 모두 리렌더링 된다.

라는 의미와 같아요.

 

자녀 컴포넌트의 입장에서는 “나는 바뀐 게 없는데 왜 다시 렌더링 돼야 하지?”라고 할 수 있겠죠. 이 부분을 돕는 도구가 바로 React.memo입니다.

 

 

(2) 코드를 통해 문제상황 살펴보기

디렉토리 구성

 

 

결과물

 

 

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;
Box1.jsx


import React from "react";

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

  // 가운데 정렬 3종세트
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
};

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",

  // 가운데 정렬 3종세트
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
};

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

export default Box2;
Box3.jsx


import React from "react";

const boxStyle = {
  width: "100px",
  height: "100px",
  backgroundColor: "#c491be",
  color: "white",

  // 가운데 정렬 3종세트
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
};

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

export default Box3;

 

 

plus 버튼 또는 minus 버튼을 누른 순간 어떻게 되나요?

 

 

모든 하위 컴포넌트가 리렌더링 되고 있습니다. 실제로 변한 것은 부모컴포넌트, App.jsx 뿐인데도 말이죠.

 

(3) memo를 통해 해결해 보기

우리는 정말 간단히 React.memo를 이용해서 컴포넌트를 메모리에 저장해 두고 필요할 때 갖다 쓰게 됩니다. 이렇게 하면 부모 컴포넌트의 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않아요. 이것을 컴포넌트 memoization이라고 합니다.

 

Box1.jsx, Box2.jsx, Box3.jsx 모두 동일

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

 

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

 

 

03. useCallback

 

(1) useCallback이란?

React.memo는 컴포넌트를 메모이제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션)해요.

 

(2) 예제를 통해 보는 useCallback의 필요성

Box1이 만일, count를 초기화해 주는 코드라고 가정해 봅시다.

 

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이 렌더링되었습니다.");

  const onInitButtonClickHandler = () => {
    initCount();
  };

  return (
    <div style={boxStyle}>
      <button onClick={onInitButtonClickHandler}>초기화</button>
    </div>
  );
}

...

 

 

 

+ 버튼이나, - 버튼을 누를 때 그리고 초기화 버튼을 누를 때 모두

 

App 컴포넌트와 Box1 컴포넌트가 리렌더링 되는 것을 볼 수 있습니다.

 

?? : React.memo를 통해서 Box1.jsx는 메모이제이션을 했는데도 리렌더링이 되네요?

네, 그것은 우리가 함수형 컴포넌트를 사용하기 때문이고 App.jsx가 리렌더링 되면서 코드가 다시 만들어지기 때문입니다.

 

const onInitButtonClickHandler = () => {
  initCount();
};

 

 

자바스크립트에서는 함수도 객체의 한 종류예요. 따라서 모양은 같더라도 다시 만들어지면 그 주소값이 달라지고 이에 따라 하위 컴포넌트인 Box1.jsxprops가 변경됐다고 인식하는 거죠.

 

(3) useCallback 사용을 통한 함수 메모이제이션

그렇기 때문에,

const onInitButtonClickHandler = () => {
  initCount();
};

이 함수를 메모리 공간에 저장해 놓고, 특정 조건이 아닌 경우엔 변경되지 않도록 해야겠어요.

 

# useCallback hook의 적용

 

 

App.jsx

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

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

 

 

이렇게 하고 나니 우리가 원하는 결과대로 Box1.jsx 컴포넌트는 리렌더링이 안되고 있네요.

 

 

(4) 조금 더 나아가기

count를 초기화할 때, 콘솔을 찍어볼게요.

App.jsx

...

// count를 초기화해주는 함수
const initCount = useCallback(() => {
  console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, []);

...

 

현재 count가 7일 때 [초기화] 버튼을 누를 거니까 콘솔에는 7에서 0으로 가 보여야겠네요.

 

하지만 실제론 어떨까요?

 

이런 현상이 발생하는 이유는, useCallbackcount가 0일 때의 시점을 기준으로 메모리에 함수를 저장했기 때문이에요. 이 때문에 우리는 dependency array가 필요합니다.

 

기존 코드의 dependency array에 count를 넣으면, count가 변경될 때마다 새롭게 함수를 할당하겠죠.

 

App.jsx

...

// count를 초기화해주는 함수
const initCount = useCallback(() => {
  console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, [count]);

...

 

 

잘 나오는 것을 볼 수 있습니다.

 

 

04. useMemo

 

(1) useMemo란?

일단, 여기서 말하는 memo는 memoization을 뜻해요. 기억한다는 말이죠. 

 

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

 

특히 복잡한 계산 결과값을 memoization 할 때 많이 사용해요.

 

 

(2) 사용방법

// as-is
const value = 반환할_함수();

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

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

그 외의 경우에는 memoization 해놨던 값을 가져오기만 해요.

 

(3) useMemo 적용해 보기 - 1

 

HeavyComponent 안에서는 const value = heavyWork()를 통해서 value값을 세팅해주고 있어요. 만약 heavyWork가 엄청나게 무거운 작업이라면 다른 state가 바뀔 때마다 계속해서 호출이 되겠죠! 하지만 useMemo()로 감싸주게 되면 그럴 걱정이 없답니다.

 

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;

 

components > HeavyComponent.jsx

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

function HeavyButton() {
  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 HeavyButton;

 

 

결과화면

 

 

(4) useMemo 적용해 보기 - 2

 

만일, 아래와 같은 코드가 있다면 어떨까요?

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 hook을 이용해서 me의 정보가 바뀌었을 때만 발동되게끔 dependency array를 넣어놨는데요. 엉뚱하게도 count를 증가하는 button을 눌러보면 계속 log가 찍히는 것을 볼 수가 있어요.

 

 

왜 그럴까요?

불변성과 관련이 깊어요.

 

위 예제에서 버튼이 선택돼서 uselessCount state가 바뀌게 되면

→ 리렌더링이 되죠

→ 컴포넌트 함수가 새로 호출됩니다.

→ me 객체도 다시 할당해요(이때, 다른 메모리 주소값을 할당받죠)

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

→ 이전 것과 모양은 같은데 주소가 달라요.

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

 

// 이건 일치해요
const a = 1;
const b = 1;

console.log(a === b); // true

// 하지만 이건 달라요
const me = {
  name: "ted chang",
  age: 21,
};

const you = {
  name: "ted chang",
  age: 21,
};

console.log(me === you); // false

 

이런 상황을 해결하기 위해서 또한 useMemo를 활용할 수 있어요!

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

 

useMemo()만 이렇게 써주면, uselessCount가 아무리 증가돼도 영향이 없게 됩니다.

 

(5) 주의해야 할 사항

useMemo를 남발하게 되면 별도의 메모리 확보를 너무나 많이 하게 되기 때문에 오히려 성능이 악화될 수 있습니다. 필요할 때만 쓰기로 합시다.

'ReactJS' 카테고리의 다른 글

React - TanStack Query  (1) 2024.09.23
React - ( React Hooks 6 - Custom Hooks )  (2) 2024.09.13
React - ( React Hooks 4 - useContext )  (0) 2024.09.06
React - ( React Hooks 3 - useRef )  (1) 2024.09.05
React - ( React Hooks 2 - useEffect )  (1) 2024.08.26