Memoization

박상욱·2022년 3월 17일

React

목록 보기
17/20

React.memo, useCallback, useMemo

기본구조

Memo <- Comments <- CommentItem
props(commentList) props(comment)

noMemo

import React from 'react';
import './CommentItem.css';
function CommentItem({ title, content, likes }) {
  return (
    <div className="CommentItem">
      <span>{title}</span>
      <br />
      <span>{content}</span>
      <br />
      <span>{likes}</span>
    </div>
  );
}

export default CommentItem;

memo를 사용하지 않을땐 하나씩 comment component가 추가될때마다 컴포넌트가 전체다 render한다.

memo

import React,{memo} from 'react';
import './CommentItem.css';
function CommentItem({ title, content, likes }) {
  return (
    <div className="CommentItem">
      <span>{title}</span>
      <br />
      <span>{content}</span>
      <br />
      <span>{likes}</span>
    </div>
  );
}

export default memo(CommentItem);


React.memo는 HOC(Higher Order Component)이다.

props가 안바꼈다면 React.memo를 React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 사용하여 성능 최적화를 할 수 있다.
memo를 사용하면 React 컴포넌트를 렌더링 하지않고 마지막으로 렌더링된 결과를 재사용한다.

이미지의 초기 결과만 봐도 1,2,3의 props가 변하지 않았다 그렇기 때문에 다시 rendering되지 않고 4만 추가한다. 이미 memoization된 컴포넌트를 다시 쓰는 것이다.

부모에서 commentList를 props로 던져주주고 부모에서 props에대한 dependcey가 바뀌면서 자식들도 다 바뀐 props에 영향을 받았는데, 자식들중에 memo를한 compoennt들은 memoization이 된 것을 쓰기때문에 반복되는 문제를 줄일 수 있다.

주의

React.memo는 props 변화에만 영향을 준다. React.memo로 감싸진 함수 컴포넌트에 useState, useReducer 또는 useContext 훅을 사용한다면, 여전히 state나 context가 변할 때 다시 렌더링됩니다.
why?

no useCallback

import React, { memo, Profiler } from 'react';
import './CommentItem.css';
function CommentItem({ title, content, likes }) {
  function onRenderCallback(
    id, // 방금 커밋된 Profiler 트리의 "id"
    phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
    actualDuration, // 커밋된 업데이트를 렌더링하는데 걸린 시간
    baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
    startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
    commitTime, // React가 해당 업데이트를 언제 커밋했는지
    interactions, // 이 업데이트에 해당하는 상호작용들의 집합
  ) {
    // 렌더링 타이밍을 집합하거나 로그...
    console.log(`actualDuration -> ${title}: ${actualDuration}`);
  }

  const handleClick = () => {
    alert(`${title} : clicked`);
  };
  
  return (
    <Profiler id={'CommentItem'} onRender={onRenderCallback}>
      <div className="CommentItem" onClick={handleClick}>
        <span>{title}</span>
        <br />
        <span>{content}</span>
        <br />
        <span>{likes}</span>
      </div>
    </Profiler>
  );
}

onClick event를 자신에게 주면 리렌더는 일어나지 않는다 하지만, 부모로부터 생성된 event handler를 받을 경우는 어떻게 될까?

import React from 'react';
import CommentItem from './CommentItem';

function Comments({ commentList }) {
  //   console.log('commentList', commentList);
  return (
    <div>
      {commentList &&
        commentList.map((comment) => {
          return (
            <CommentItem
              key={comment.title}
              title={comment.title}
              content={comment.content}
              likes={comment.likes}
              onClick={() => console.log('눌림')}
            />
          );
        })}
    </div>
  );
}
import React, { memo, Profiler } from 'react';
import './CommentItem.css';
function CommentItem({ title, content, likes, onClick }) {
  function onRenderCallback(
    id, // 방금 커밋된 Profiler 트리의 "id"
    phase, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
    actualDuration, // 커밋된 업데이트를 렌더링하는데 걸린 시간
    baseDuration, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
    startTime, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
    commitTime, // React가 해당 업데이트를 언제 커밋했는지
    interactions, // 이 업데이트에 해당하는 상호작용들의 집합
  ) {
    // 렌더링 타이밍을 집합하거나 로그...
    console.log(`actualDuration -> ${title}: ${actualDuration}`);
  }

  const handleClick = () => {
    onClick();
    alert(`${title} : clicked`);
  };

  return (
    <Profiler id={'CommentItem'} onRender={onRenderCallback}>
      <div className="CommentItem" onClick={handleClick}>
        <span>{title}</span>
        <br />
        <span>{content}</span>
        <br />
        <span>{likes}</span>
      </div>
    </Profiler>
  );
}

export default memo(CommentItem);

React.memo를 설정 했음에도 불과하고 다시 다생기는 모습을 볼 수 있다.
CommentList에서 념겨준 onClick은 inline으로 만들어진거기 때문에 새로 렌더링 될때마다 새로만들어지기 때문에 props가 변경되었다고 판단하고 새로 다 그린다. 그럼 line말고 함수로 빼서 만들면 문제가 해결될까?

결과는 '아니다!'
상위의 Memo로 부터 전달받은 commentList가 변경되서 Comments자체가 rerender되기 때문에 따로 빼놓은 함수도 새로 그려진다.
이때 사용하는 것이 useCallback이다.

useCallback

function Comments({ commentList }) {
  //   console.log('commentList', commentList);
  const handleClick = useCallback(() => {
    console.log('click!');
  }, []);
  return (
    <div>
      {commentList &&
        commentList.map((comment) => {
          return (
            <CommentItem
              key={comment.title}
              title={comment.title}
              content={comment.content}
              likes={comment.likes}
              onClick={handleClick}
            />
          );
        })}
    </div>
  );
}

export default Comments;

commentItem에 전달하는 handleClick또한 memoization이 되기 때문에 전달받아도 rerender 되지 않는다.

no useMemo

만약 rate라는 like 수에따라 변화를주는 변수를 생성한다고 해보자

import React, { memo, Profiler } from 'react';
import './CommentItem.css';
function CommentItem({ title, content, likes, onClick }) {
  const handleClick = () => {
    onClick();
    alert(`${title} : clicked`);
  };

  const rate = () => {
  	console.log('rate check');
    return likes > 10 ? 'good' : 'bad';
  };

  return (
      <div className="CommentItem" onClick={handleClick}>
        <span>{title}</span>
        <br />
        <span>{content}</span>
        <br />
        <span>{likes}</span>
        <br />
        <span>{rate()}</span>
      </div>
  );
}

export default memo(CommentItem);

component를 하나 생성할 때 마다 하나씩 잘찍힐 것이다.
하지만 click했을때 몇번이나 클릭되는지 아는 state를 추가해보자

import React, { memo, Profiler,useState } from 'react';
import './CommentItem.css';
function CommentItem({ title, content, likes, onClick }) {
  const [clickCount, setclickCount] = useState(0);
  
  const handleClick = () => {
    onClick();
    setclickCount((prev) => prev + 1);
    alert(`${title} : clicked`);
  };
  const rate = () => {
    return likes > 10 ? 'good' : 'bad';
  };

  return (
      <div className="CommentItem" onClick={handleClick}>
        <span>{title}</span>
        <br />
        <span>{content}</span>
        <br />
        <span>{likes}</span>
        <br />
        <span>{rate()}</span>
        <br />
        <span>{clickCount}</span>
      </div>
  );
}

export default memo(CommentItem);


item을 한번 클릭할때마다 item이 count가 각각 동적으로 바뀌는 상황이다.
근데 상태가 바뀌었으니 render를 다시 그리는건 맞는데, rate check를 다시한다? rate check의 값은 변하지 않았지만 해당 component가 바뀌었기때문에 rerender되고 memoization되지 않은 rate()함수를 다시 그리는 것이다.
이문제를 해결하기 위해서 사용하는것이 useMemo이다.

useMemo

import React, { memo, useState, useMemo } from 'react';
import './CommentItem.css';

function CommentItem({ title, content, likes, onClick }) 

  const [clickCount, setclickCount] = useState(0);
  
  const handleClick = () => {
    onClick();
    setclickCount((prev) => prev + 1);
    alert(`${title} : clicked`);
  };

  const rate = useMemo(() => {
    return likes > 10 ? 'good' : 'bad';
  }, [likes]);

  return (
    <Profiler id={'CommentItem'} onRender={onRenderCallback}>
      <div className="CommentItem" onClick={handleClick}>
        <span>{title}</span>
        <br />
        <span>{content}</span>
        <br />
        <span>{likes}</span>
        <br />
        <span>{rate}</span>
        <br />
        <span>{clickCount}</span>
      </div>
    </Profiler>
  );
}

export default memo(CommentItem);

useMemo도 dependency사용 방법은 다른 hook들과 같다.
다른점은 useMemo는 값을 가지고 있는 것이기 때문에 함수가아닌 return되는 값으로 받아줘야한다 그래서 <span>{rate()}</span>코드를 <span>{rate}</span> 이렇게 변경해 주었다.

다시 클릭햇을때 memoization된 값을 사용함으로 다시 호출 하지 않는다.

useCallback vs useMemo

useCallback

  • 어떤 함수를 메모이제이션 하는것

useMemo

  • 어떤 계산을 통해 만들어진 특정한 값을 메모이제이션 하는것
profile
개발자

0개의 댓글