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

Nick·2023년 6월 23일
0

TIL: 오늘을 돌아보자

목록 보기
30/95
post-thumbnail

최적화

리렌더링의 발생 조건
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
배우고 도전하는것을 멈추지 않는 개발자 입니다.

0개의 댓글

관련 채용 정보