[React] 성능 최적화

I'm ·2023년 4월 17일

계속 새롭게 정의되지 않는 그냥 변수들도 useMemo에 감싸주는걸까?

🤍 Memoization

  • 리렌더링 될 때마다, 계속 새로 만들어지는게 좋은 건 아닐지도 몰라.

  • 자꾸 새로 만들지 말고, 메모해 놓는거 어때? => Memoization

  • 메모리를 비우는 작업 => 가비지 컬렉션
    이때, 멈춤 현상이 일어납니다. 메모리를 비우는 작업이 자주 일어날 수록 좋지 않겠죠.

📃 useMemo

  • 새로 만들지 않고, 이미 만들어졌던 거 메모해뒀다가 가져다 씁니다.
    useEffect와 형태가 비슷합니다.
  const aaa = useMemo(() => Math.random(), []);
  • 근데 계속 같은 값을 가지고 싶지 않을 수도 있잖아요.
    만약 새롭게 초기화하고 싶다면?
    [ ] => dependency array를 적용해주면 됩니다.
 const aaa = useMemo(() => Math.random(), [countState]);
  • countState가 변할 때마다 변수 값을 새롭게 해줍니다.

📃 useCallback

  • 변수는 useMemo, 함수는 useCallback을 사용하여 기억합니다.
  const onClickCountLet = useCallback(() => {
    console.log(countLet + 1);
    countLet += 1; // countLet = countLet + 1
  }, []);

  const onClickCountState = useCallback(() => {
    console.log(countState + 1);
    setCountState(countState + 1);
  }, []);
  • 잘못쓰게 되는 case
  const onClickCountState = useCallback(() => {
    console.log(countState + 1);
    setCountState(countState + 1);
  }, []);
  • 함수를 memo할 때, 그 안에 state가 있으면 그 state값까지 memo되기 때문에 조심해야합니다.
    countState의 값을 기억해서 1씩 더하지 않고 계속 1로 남아있게 됩니다.
  • 따라서, 아래와 같이 바꿔줍니다.
  const onClickCountState = useCallback(() => {
    // console.log(countState + 1);
    setCountState(prev => prev + 1);
  }, []);

prev를 사용해요.

  • useMemo로 나만의 useCallback 만들어보기(이렇게는 많이 사용하지 않음)
  const onClickCountState = useMemo(
    () => () => {
      console.log(countState + 1);
      setCountState(countState + 1);
    },
    [],
  );

🤍 컴포넌트 단위에서의 메모이제이션

  • 부모의 리렌더가 자식에 미치는 영향
    부모컴포넌트가 리렌더 되면 자식컴포넌트 또한 리렌더 되게 됩니다.
    하지만 자식컴포넌트에 변화된 값이 없는데 리렌더가 일어난다면 성능면에서 비효율적입니다.

📃 React.memo

부모 컴포넌트가 리렌더링 될 때마다 자식 컴포넌트도 리렌더링 되는 상황에 사용합니다.

import { memo } from "react";

function MemoizationChildPage() {
  console.log("자식이 렌더링 됩니다!");

  return (
    <>
      <div>=========================</div>
      <h1>저는 자식 컴포넌트 입니다!!!</h1>
      <div>=========================</div>
    </>
  );
}

export default memo(MemoizationChildPage);
  • memo에서 dependency array는 props입니다.
  • props가 바뀌면 refresh되고, 안바뀌면 그냥 memo가 유지됩니다.
import Word from "./02-child";
import { v4 as uuidv4 } from "uuid";

export default function MemoizationParentPage() {
  const [data, setData] = useState("철수는 오늘 점심을 맛있게 먹었습니다");

  const onClickChange = () => {
    setData("영희는 오늘 저녁을 맛없게 먹었습니다");
  };

  return (
    <>
      {data.split(" ").map((el, index) => {
        return <Word key={index} el={el} />; // 1. memo시, key 또는 el이 변경된 부분만 리렌더링 됨(즉, "오늘", "먹었습니다" 제외)
      })}
      {data.split(" ").map((el, index) => {
        return <Word key={uuidv4()} el={el} />; // 2. memo를 해도, key 자체가 변경되어 props로 넘어가므로, 모두 리렌더링 됨
      })}
      <button onClick={onClickChange}>체인지</button>
    </>
  );
}
function Word(props: any) {
  console.log("자식이 렌더링 됩니다!", props.el);

  return (
    <>
      <span>{props.el}</span>
    </>
  );
}

export default memo(Word);
  • memo를 걸어두면 처음의 값이 기억되어 el이 변경된 부분만 리렌더링 됩니다. 하지만 key값에 index가 아니라 uuid를 사용하게 되면 상황이 달라집니다.
    uuid를 사용하면 memo를 걸어놔도 key값이 변경되어 props로 넘어가기 때문에 변경된 부분이 모두 리렌더링 됩니다.
    uuid는 불필요한 리렌더링을 초래하므로 사용하실때는 필요한 상황에서만 주의해서 사용해야합니다.

💥 모든 컴포넌트에 memo, useCallback을 사용했을 때의 문제점

  • 다 memo를 붙이면 되는 거 아닌가? 라고 생각할 수 있다. 그렇다면 react가 처음부터 memo를 붙여서 사용하도록 만들었을 것이다.
  • 기억을 해둔다는 것은 컴퓨터 메모리 어딘가에 저장을 해둔다는 것입니다. 따라서 자식이 리렌더 될 일이 없는 컴포넌트에 memo를 붙이면 필요없는 메모리 낭비를 하게 되는 것 입니다.

💥 메모이제이션 쓰지 말아야할 때,
dependency array가 2개 이상 넘어가면 굳이 써야할까? 그냥 항상 refresh 되도록 하는 것이 유지보수 측면에서도 좋을 수 있다.
무조건 성능이 좋다고 좋은 건 아니다.

[참고 1]

  • react-dev-tools : 크롬 확장프로그램
    Profile의 녹화를 통해 memo를 붙이면 자식 컴포넌트는 리렌더링이 안되는 것을 확인할 수 있습니다.

[참고 2]
가비지 컬렉션 => 안쓰는 메모리 지워주기

  • 언어에는 언매니지드언어(C, C++), 매니지드언어(Java, Python, JS)가 있다.

  • 언매니지드언어는 가비지컬렉션 같은 작업을 직접 사람이 해줘야한다.
    "지금 이 타이밍에 메모리 비워줘."와 같은 코드 작성해야한다.
    따라서 불편하지만 효율은 높다.

  • 매니지드언어는 직접 코드를 작성해줄 필요가 없어 편리하지만 효율은 떨어진다.

  • memo가 되지 않은 map의 uuid key 리렌더링 문제
    uuid key를 사용하면 key값이 계속 변하기 때문에

  • 메모이제이션 사용 사례 소개
    관리자 페이지 테이블

profile
프론트엔드 개발 공부

0개의 댓글