[TIL 221125] Memoization

ponyo·2022년 11월 25일
0

Today I Learned

목록 보기
2/30

메모이제이션은 컴퓨터 프로그램이 동일한 계산반복해야할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술

React.memo

동일한 props로 렌더링을 한다면, React.memo를 사용하여 성능 향상을 누릴 수 있습니다.

memo를 사용하면 React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과재사용합니다.

Memo.jsx

import React, { useState, useEffect } from 'react'
import Comments from './Comments';

const commentList = [
  { title: "comment1", content: "message1", likes: 1},
  { title: "comment2", content: "message2", likes: 1},
  { title: "comment3", content: "message3", likes: 1},
];

export default function Memo() {
  const [comments, setComments] = useState(commentList);

  useEffect(() => {
    const interval = setInterval(() => {
      setComments((prevComments) => [
        ...prevComments,
        { title: `comment${prevComments.length + 1}`, content: `message${prevComments.length + 1}`, likes: 1},
      ])
    }, 1000);
    return () => {
      clearInterval(interval);
    }
  }, [])
  

  return (
    <Comments commentList={comments} />
  )
}

useEffect 코드의 실행 결과로 comment가 1개씩 계속 증가

useEffect(() => {
    const interval = setInterval(() => {
      setComments((prevComments) => [
        ...prevComments,
        { title: `comment${prevComments.length + 1}`, content: `message${prevComments.length + 1}`, likes: 1},
      ])
    }, 1000);
    return () => {
      clearInterval(interval);
    }
  }, [])

Comments.jsx

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

export default function Comments({ commentList }) {
  return (
    <div>
      {commentList.map(comment => <CommentItem
        key={comment.title}
        title={comment.title}
        content={comment.content}
        likes={comment.likes}
      />)}
    </div>
  )
}

CommentItem.jsx (Profiler API란?)

import React, { Profiler, memo } 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})`);
  }
  return (
    <Profiler id="CommentItem" onRender={onRenderCallback}>
      <div className="CommentItem">
        <span>{title}</span>
        <br />
        <span>{content}</span>
        <br />
        <span>{likes}</span>
        <br />
      </div>
    </Profiler>
  );
}

export default CommentItem;

콘솔 결과

useEffect setInterval로 인해 comment가 1초에 한개씩 증가하는데 console창을 보면

1
2
3을 그리고
4가 바로 추가되는게 아니라
//
1
2
3을 그리고
1
2
3
4로 123을 다시 그리고 4를 추가한다.

즉, 1초 뒤에는 다시
1
2
3
4를 그리고
5가 추가됨을 예상할 수 있다.

한 개의 컴포넌트가 추가될 때마다 모든 컴포넌트들이 다 새로 그려진다. 너무 비효율적이다 ❗

export default memo(CommentItem);

memo를 적게되면 아래 이미지처럼 추가된 컴포넌트만 그린다.

왜냐하면 아래 내용처럼 props가 바뀌지 않기 때문에!!

동일한 props로 렌더링을 한다면, React.memo를 사용하여 성능 향상을 누릴 수 있습니다.

useCallback

위에서 React.memo는 동일한 props로 렌더링 할 때, 전체를 다시 그리는 것이 아니라 추가된 컴포넌트만 그리게끔 했는데 useCallback은 언제 사용하는 것일까 ?

위 코드에서 실험한 Component는 CommentItem으로 아래와 같이 title, content, likes를 props로 받았다.

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})`);
  }
  return (
    <Profiler id="CommentItem" onRender={onRenderCallback}>
      <div className="CommentItem">
        <span>{title}</span>
        <br />
        <span>{content}</span>
        <br />
        <span>{likes}</span>
        <br />
      </div>
    </Profiler>
  );
}

만약에 onClick을 부모한테 props로 받는다면 어떻게 작동할까?

Comment.jsx (부모)

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

export default function Comments({ commentList }) {
  return (
    <div>
      {commentList.map(comment => <CommentItem
        key={comment.title}
        title={comment.title}
        content={comment.content}
        likes={comment.likes}
        onClick={() => console.log('눌림')} // 추가
      />)}
    </div>
  )
}
// onClick props 추가
CommentItem({ title, content, likes, onClick })

// handleClick 정의
const handleClick = () => {
  onClick();
}
// 자식인 CommentItem에 onClick event 추가
<div className="CommentItem" onClick={handleClick}>

이 상태에서 onClick 하게 되면 memo를 했음에도 불구하고 모든 컴포넌트들이 전부 그려짐.

  1. 자식이 부모한테서 onClick props를 받아옴
  2. 부모한테서 받아온 onClick props는 인라인으로 어떤 실행을 하는 코드임
  3. 함수를 인라인으로 만들어주게 되면 렌더링 될 때마다 새로운 함수를 만드는 것임
  4. memo는 props가 동일한 경우에만 다시 그리지 않는데 이 경우 props가 바뀌는 것으로 간주되어서 전체를 다시 그림

Comments.jsx (부모)

onClick={() => console.log('눌림')}

이 코드를

const handleChange = () => {
  console.log('눌림');
}

onClick={handleChange}

으로 바꾸더라도 Comments가 리렌더링 되기 때문에 함수가 계속 새로 생성되어서 모든 컴포넌트가 다시 그려진다.

💡 이 때 usCallback을 사용

const handleChange = useCallback(() => {
  console.log('눌림');
}, []);

onClick={handleChange}

이제 추가된 컴포넌트만 그려진다.

useMemo

요구 사항이 추가되었다.
likes 가 10이 넘으면 rate 값에 string으로 Good을 넣고 아니면 Bad를 넣자.

const rate = () => {
  console.log('rate check');
  return likes > 10 ? 'Good' : 'Bad';
}
function CommentItem({ title, content, likes, onClick }) {
  return (
     <div className="CommentItem" onClick={handleClick}>
       <span>{title}</span>
       <br />
       <span>{content}</span>
       <br />
       <span>{likes}</span>
       <br />
      <span>{rate()}</span>
     </div>
  );
}

그럼 이제 CommentItem이 그려질 때마다 rate check가 콘솔에 찍힐 것이다.

그리고 자신이 몇번 클릭되었는 지를 count하는 값을 handleClick 이벤트에 추가해주자.

const [clickCount, setClickCount] = useState(0);

const handleClick = () => {
  setClickCount((prev) => prev + 1);
}

자 이제 CommentItem을 클릭해보자 ! clickCount의 값이 바뀌기 때문에 컴포넌트가 다시 그려진다. 하지만 우리는 위에서 Memoization을 잘해줬기 때문에 전체가 그려지지 않고 클릭된 컴포넌트만 다시 그려질 것이다. 그려지는 과정에서 clickCount 값이 1 증가 된다.

이 때 중요한 것은

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

위 rate 함수도 다시 실행된다는 점이다.

clickCount 상태값이 변했기 때문에 다시 그려지는 것은 당연한 것이지만 rate 함수가 다시 실행될 필요는 없다.

rate 함수를 useMemo를 이용하면 이 문제는 해결된다.

const rate = useMemo(() => {
  console.log('rate check');
  return likes > 10 ? 'Good' : 'Bad';
}, [likes]);
// 기존 코드 <span>{rate()}</span>

// 바뀐 코드
// useMemo를 쓰면 기존 코드처럼 따로 함수를 실행할 필요없다
<span>{rate}</span> 

이렇게 바꾸고 나면 clickCount가 바뀌는 등 rate와 관련없는 동작으로 다시 렌더링 될 때 rate가 실행되지 않고 likes가 바뀌었을 때만 실행된다.


요약

특정한 값을 Memoization 할 때는 useMemo
특정한 함수를 Memoization 할 때는 useCallback

profile
😁

0개의 댓글