[TIL]2023.06.22 최적화(React.memo, useCallback, useMemo)

Nick·2023년 6월 23일
0

TIL: 오늘을 돌아보자

목록 보기
30/95
post-thumbnail
post-custom-banner

최적화

리렌더링의 발생 조건
1. 컴포넌트에서 state가 바뀌었을 때
2. 컴포넌트가 내려받은 props가 변경됐을 때
3. 부모 컴포넌트가 리렌더링 된 경우

리액트에서 리렌더링이 자주 일어나는 것은 그다지 좋은 현상이 아니기 때문에 비용(cost)을 줄여야 한다.
비용을 줄이기 위한 방법이 최적화이다.

최적화 방법

대표적인 방법
1. memo: 컴포넌트를 캐싱(메모이제이션)
2. useCallback: 함수를 캐싱(메모이제이션)
3. useMemo: 값을 캐싱(함수가 return하는 값 또는 값 자체)

캐싱이란 메모리에 저장하는 것이다.
아래와 같은 부모 컴포넌트가 있을 때

import React, { useState } from "react";
import "./App.css";
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 increaseBtnHandler = () => {
    setCount(count + 1);
  };
  const decreaseBtnHandler = () => {
    setCount(count - 1);
  };
  return (
    <>
      <h3>카운트 예제</h3>
      <p>현재 카운트 {count}</p>
      <button onClick={increaseBtnHandler}>+</button>
      <button onClick={decreaseBtnHandler}>-</button>
      <div
        style={{
          display: "flex",
          margin: "10px",
        }}>
        <Box1 />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

export default App;

React.memo

버튼을 클릭하여 부모 컴포넌트가 리렌더링 될 때 자식 컴포넌트는 변경이 없어도 같이 리렌더링 된다. 이 문제를 해결하는 방법은 간단하다.

export default React.memo(Box1);

자식 컴포넌트를 export시 컴포넌트 이름을 React.memo()로 감싸는 것이다.

useCallback

위의 부모 컴포넌트에 아래와 같이 count를 초기화하는 함수가 있고

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

아래와 같은 그 함수를 props로 받는 자식 컴포넌트가 있을 때

import React from "react";

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

function Box1({ initCount }) {
  console.log("box1 렌더링");
  return (
    <div style={style}>
      <button
        onClick={() => {
          initCount();
        }}>
        초기화
      </button>
    </div>
  );
}

export default React.memo(Box1);

React.memo를 사용해도 부모 컴포넌트가 리렌더링 되면 리액트는 props로 받는 함수가 변경됐다고 판단하여 해당 props를 물려받는 자식 컴포넌트도 리렌더링된다.(부모 컴포넌트의 +나 -버튼을 눌렀을 때)

이 문제는 아래와 같이 해당 함수를 useCallback으로 감싸주면 되며 useCallback은 의존성 배열이 필요하다.

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

의존성 배열이 빈 배열이면 의존성 배열에 새로운 값이 추가되거나 변경되지 않으니 initCount 함수는 그대로 유지, 즉 props가 변경되지 않는 것이다.

하지만 initCount함수 내에서 현재 count값을 사용해야 한다면 의존성 배열에 count를 넣어줘야 한다.

왜냐하면 useCallback으로 감싸면 count값은 함수는 처음 저장된 시점, 즉 count값이 초기값 0인 시점 상태기 때문에 의존성 배열에 count를 넣지 않으면 initCount함수 내의 count값은 0이다.(snapshot을 유지하기 때문)

useMemo

useMemo는 함수의 값, 즉 return하는 값을 캐싱하는 방법이다.

만약 아래와 같이 시간이 오래 걸리는(무거운) 컴포넌트가 있다고 하자.

import React, { useState } from "react";

function HeavyComponent() {
  const [count, setCount] = useState(0);
  const heavyWork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };
  
  const value = heavyWork();
  console.log(value);
  return (
    <>
      <p>Im HeavyComponent</p>
      <p>{count}</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}>
        count +
      </button>
    </>
  );
}

export default HeavyComponent;

heavyWork함수의 값을 value에 저장해서 콘솔에 출력을 한다고 했을 때

버튼을 클릭 => state변경 => 리렌더링 이 과정에 딜레이가 생기는 것을 볼 수 있다.

이런 문제를 해결하기 위해 useMemo를 사용한다. 사용하는 방법은 아래와 같다.

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

useMemo로 감싸면 버튼을 클릭해도 딜레이가 생기지 않는다.


useMemo 예시 2

아래와 같은 코드가 있다면

import React, { useEffect, useMemo, 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;

필요없는 숫자 영역의 버튼을 클릭했을 때 콘솔이 출력되는 것을 볼 수 있다.

uselessCount의 state가 변경이 되면서 함수 자체(컴포넌트)가 리렌더링이 되고,

리렌더링 과정에서 me라는 객체의 주소값이 바뀌기 때문에

리액트는 useEffect의 의존성 배열 안에 있는 me라는 객체가 바뀌었다고 판단하기 때문이다.

이 문제를 해결하기 위해서는 me라는 객체를 아래와 같이 useMemo로 감싸면 되고

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

마찬가지로 의존성 배열이 필요하다.

주의할 점은 useMemo, useCallback을 통해서 저장하는 값은 메모리에 임시적으로 저장되기 때문에 남발하면 메모리 성능을 저하시킨다.

profile
배우고 도전하는것을 멈추지 않는 개발자 입니다.
post-custom-banner

0개의 댓글