본문 바로가기

React/성능,코드 최적화

성능 최적화 - 메모이제이션 ( React.memo, useMemo, useCallback )

useMemo 와 useCallback 은 불필요한 자식 요소의 재렌더링이나 계산을 억제하기 위해 사용합니다.

 

설명에 앞서 리액트의 컴포넌트의 재렌더링되는 시점을 보면

  • props 값이나 state(상태값) 이 변경 되었을 때
  • 컴포넌트에서 참조되는 Context 값이 변경 되었을 때
  • 부모 컴포넌트가 렌더링 되었을 때

이렇게 크게 세가지로 인해 자식 컴포넌트에서 불필요한 재렌더링이 일어납니다.

 

 

React.memo

함수 컴포넌트를 메모이제이션할 수 있다. 부모의 컴포넌트가 제렌더링 되었을 때 자식 컴포넌트도 재랜더링이 일어나게 되어있습니다.

하지만 React.memo를 사용하면 props의 값이 변하지 않는 이상 자식 컴포넌트의 재렌더링을 방지 할 수 있습니다.

 

아래 예시를 보면 Child_1 컴포넌트는 메모이제이션을 하지 않았고 Child_2 컴포넌트는 메모이제이션을 사용하였다.

Parent의 count 값이 증가하면서 Child 컴포넌트들에 props 값이 변경되게 됩니다.

import { memo, useState } from "react";

/* Child 1 memo (X) */
interface Child_1Prop {
  isChild: boolean;
}
const Child_1 = ({ isChild }: Child_1Prop) => {
  console.log(`Child_1 이 다시 그려졌습니다. isChild: ${isChild}`);
  return <div>{isChild ? "Child_1" : ""}</div>;
};

/* Child 2 memo (O) */
interface Child_2Prop {
  isChild: boolean;
}
const Child_2 = memo(({ isChild }: Child_2Prop) => {
  console.log(`Child_2 이 다시 그려졌습니다. isChild: ${isChild}`);
  return <div>{isChild ? "Child_2" : ""}</div>;
});

/* Parent */
export const Parent = () => {
  const [count, setCount] = useState<number>(1);
  const isChild_1 = count % 3 === 0;
  const isChild_2 = count % 5 === 0;

  console.log(`Parent 가 그려졌습니다. count => ${count}`);

  const handleClick = () => {
    setCount((prev) => prev + 1);
  };
  return (
    <div>
      <button onClick={handleClick}>+1</button>
      <p>현재 카운트 : {count}</p>
      <div>
        <Child_1 isChild={isChild_1} />
        <Child_2 isChild={isChild_2} />
      </div>
    </div>
  );
};

 

첫번째 렌더링시 모든 컴포넌트가 렌더링 됩니다.

 

 

 

+1 버튼을 눌러 count 의 state 값이 변경되면서 Parent 컴포넌트는 재렌더링이 일어나게 됩니다.

Child_1 컴포넌트는 부모가 재렌더링이 일어나면서 해당 컴포넌트도 재렌더링이 일어나게 됩니다. 

Child_2 컴포넌트는 메모이제이션이 된 컴포넌트로 부모의 컴포넌트가 재렌더링이 일어나더라도 props로 넘어온 isChild값이 변경되지 않는다면 재렌더링이 일어나지 않습니다.

 

 

 

useCallback

useCallback  은 함수를 메모이제이션하기 위한 hook입니다.

 

Child_1 => memo (X), useCallback (X)

Child_1 컴포넌트는 memo를 사용하지 않는 컴포넌트로 부모 컴포넌트가 재렌더링 될 때마다 재렌더링이 일어납니다.

 

Child_2 => memo (O), useCallback (X)

Child_2 컴포넌트는 memo를 사용하였지만 부모 컴포넌트로부터 받은 onClick 함수가 업데이트 될 때마다 재렌더링이 일어납니다.

 

Child_3 => memo (X), useCallback (O)

Child_3 컴포넌트는 useCallback 를 사용하여 onClick 함수가 업데이트 되더라도 해당 함수를 업데이트하지 않아야하지만 memo를 사용하지 않는 컴포넌트이므로 부모 컴포넌트가 재렌더링 될 때마다 재렌더링이 일어납니다.

 

Child_3 => memo (O), useCallback (O)

Child_1 컴포넌트는 memo를 사용하여 부모 컴포넌트가 재렌더링 될 때 해당 컴포넌트는 재렌더링을 방지합니다. 

또한 useCallback 를 사용한 onClick 함수를 props로 전달 받았으므로 부모 컴포넌트의 onClick 함수가 업데이트 되더라도 해당 컴포넌트는 처음에 받은 onClick 함수를 사용하므로 재렌더링이 일어나지 않습니다.

 

import { memo, useCallback, useState } from "react";

/* Child 1 memo (X), useCallback (X) */
interface Child_1Prop {
  onClick: () => void;
}
const Child_1 = ({ onClick }: Child_1Prop) => {
  console.log(`Child_1 이 다시 그려졌습니다`);
  return <button onClick={onClick}>{"Child_2"}</button>;
};

/* Child 2 memo (O), useCallback (X) */
interface Child_2Prop {
  onClick: () => void;
}
const Child_2 = memo(({ onClick }: Child_2Prop) => {
  console.log(`Child_2 이 다시 그려졌습니다`);
  return <button onClick={onClick}>{"Child_2"}</button>;
});

/* Child 3 memo (X), useCallback (O) */
interface Child_3Prop {
  onClick: () => void;
}
const Child_3 = ({ onClick }: Child_3Prop) => {
  console.log(`Child_3 이 다시 그려졌습니다`);
  return <button onClick={onClick}>{"Child_3"}</button>;
};

/* Child 4 memo (O), useCallback (O) */
interface Child_4Prop {
  onClick: () => void;
}
const Child_4 = memo(({ onClick }: Child_4Prop) => {
  console.log(`Child_4 이 다시 그려졌습니다`);
  return <button onClick={onClick}>{"Child_4"}</button>;
});

/* Parent */
export const Parent = () => {
  const [count, setCount] = useState<number>(1);

  console.log(
    `------------------------- Parent 가 그려졌습니다. count => ${count} -------------------------`
  );
  const handleClickMemoization = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);
  const handleClick = () => {
    setCount((prev) => prev + 1);
  };
  return (
    <div>
      <p>현재 카운트 : {count}</p>
      <div>
        <Child_1 onClick={handleClick} />
        <Child_2 onClick={handleClick} />
        <Child_3 onClick={handleClickMemoization} />
        <Child_4 onClick={handleClickMemoization} />
      </div>
    </div>
  );
};

 

 

 

useMemo

useMemo는 값을 메모이제이션하기 위한 hook 입니다.

 

value_1 의 값은 그릴때 마다 호출되어 값을 변경합니다. items의 값이 변경되지 않은 상태에서도 계산이 일어나는건 성능저하가 일어날 수 있습니다.

value_2 의 값은 useMemo로 값을 메모이제이션한 값으로 items의 값이 변경이 일어 났을 때만 호출 되어 성능 저하 방지를 할 수 있습니다.

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

export const UseMemoSample = () => {
  const [text, setText] = useState<string>("");
  const [items, setItems] = useState<string[]>([]);

  const handleOnChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };
  const handleOnClickBtn = () => {
    setItems((prve) => {
      return [...prve, text];
    });
    setText("");
  };
  const value_1 = items.reduce((sub, item) => sub + item.length, 0);
  const value_2 = useMemo(() => {
    return items.reduce((sub, item) => sub + item.length, 0);
  }, [items]);

  return (
    <div>
      <div>
        <input value={text} onChange={handleOnChangeInput}></input>
        <button onClick={handleOnClickBtn}>Add</button>
      </div>
      <div>
        {items.map((item, idx) => {
          return <p key={idx}>{item}</p>;
        })}
      </div>
      <div>
        <p>Total Value_1 : {value_1}</p>
        <p>Total Value_2 : {value_2}</p>
      </div>
    </div>
  );
};