[React] 메모..(memoization)

eastZoo·2024년 5월 24일

React

목록 보기
14/15
post-thumbnail

0️⃣ 들어가며

🙋🏻 : 메모이제이션 이란 뭔가요?

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



1️⃣ react 메모이제이션

리액트에서 제공하는 메모이제이션 기법은 아래 메소드들을 통해서 사용할 수 있다

🏷️ 1. React.memo(컴포넌트)

React.memo() 는 props의 값으로 변경을 확인을하고

  • 컴포넌트 자체를 메모이제이션

  • 자체적으로 props값을 비교해서 달라진 부분이 없다면 리액트 DOM에서 비교 작업이 들어가지 않는다.

동일한 props로 렌더링을 한다면, React.memo를 사용하여 성능향상을 기대할 수 있다.
memo를 사용하면 React는 컴포넌트 전체를 렌더링 하지않고 마지막으로 렌더링된 결과를 재사용한다.

예제를 통해 알아보자.



💬 예시

먼저 memo를 쓰지않고 컴포넌트가 여러번 렌더링되는 것을 아래 예시를 통해 알아보자

Memo앱이 App컴포넌트에 들어가는 가장 최상위 컴포넌트로

App>Memo>Comments>CommentItem 순의 컴포넌트이다.

  • 파일구조 🗂️
📦src
 ┣ 📂Components
 ┃ ┗ 📂Memo
 ┃ ┃ ┣ 📜Comments.jsx
 ┃ ┃ ┣ 📜CommentItem.jsx
 ┃ ┃ ┗ 📜Memo.jsx
 ┣ 📜App.css
 ┣ 📜App.jsx
 ┣ 📜index.css
 ┗ 📜index.jsx

아래컴포넌트는 useEffect 함수의 안에 있는 interval 함수에서
setCommets를 통해 1초에 한번씩 commentList가 담겨있는 comments 변수에 새로운 리스트를 추가한다.
( 추가될때 마다 Comments 컴포넌트에 새로운 값을 넘기는 상황 발생 )

🐸 : interval함수에 대해 이해가 안된다면 일단 그냥 넘어가볼까요? 지금은 메모 기능에대해 살펴보는 중이니까요, 아래 코드에서 interval 함수는 그냥 기존의 컴포넌트에 하나씩 요소를 추가해가며 테스트하기 위한 장치정도로 이해하고 넘어가요!

  • App.jsx
import React, { useEffect, useState } from "react";
import Memo from "./Components/Memo/Memo";

function App() {
  return (
    <>
      <Memo />
    </>
  );
}

export default App;

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

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

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

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

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

위의 Memo 컴포넌트에서 props로 받아온 값을 map함수를 통해 아래의 Comments컴포넌트로 전달하고 있다.

  • 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>
  )
}

다시한번 위의 Comments에서 받아온 props를 아래의 CommentItem에 전달하여 화면에 출력한다.

💡 이때 여기서 리액트가 컴포넌트를 렌더링하는 빈도를 확인할 수있는 React의 Profiler 기능을 사용한다.

Profiler 공식문서 : https://react.dev/reference/react/Profiler

Profiler을 사용하기위해 공식문서에 있는 대로 onRender 함수를 추가하고

console.log(`actualDuration(${title}: ${actualDuration})`);

함수 안에서 렌더링 빈도를 확인하기 위한 콘솔출력문을 추가해준뒤.

Profiler 태그로 감싸준다.


  • CommentItem
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})`);
  }

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

export default CommentItem

memo를 사용하지 않은 결과

위와같이 새로운 컴포넌트는 계속해서 하나씩만 추가되고 있지만 Profiler를 통해 출력한
렌더링 빈도를 보면 이전에 렌더링되었던 props값을 포함해 계속해서 렌더링하는 것을 확인할 수 있다.

🐸 : 직접 실습으로 따라한뒤 콘솔을 보시면 더 잘보일거에요
초반(1,2,3) 컴포넌트 렌더링 후 interval 함수에 의해 하나더 추가될 때 4번만 렌더링 하는게 아니라 1,2,3을 포함한 1,2,3,4 모두를 다시 렌더링하는걸 볼 수 있어요
ex ) 1,2,3 => 1,2,3,4 => 1,2,3,4,5



📝 React.memo 사용시

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})`);
  }

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

export default memo(CommentItem)

리액트 메모를 import하고

export default memo(CommentItem)

export할 함수를 memo로 감싸줌으로써 사용한다.

위와 같이 이전의 렌더링된 props값들은 다시 렌더링되지 않고 새로 생성되어 보내져온 props 값들만
CommentItem을 통해 새로 그려지는 것을 확인할 수 있다.



✋잠깐 !!! 그런데 말입니다...

🐸 : 하지만 생성된 요소들을 클릭하는 인라인 동작 함수, 또는 함수를 props로 추가하게 된다면
memo를 사용하고 있음에도 불구하고 또 다시 이전값들과 함께 매번 다시 렌더링하기 시작해요.

import React, { useCallback } from 'react'
import CommentItem from './CommentItem'

export default function Comments({ commentList }) {
    const onSubmit = () => {
    console.log("submit");
  };
  return (
    <div>
      {commentList.map((comment) => <CommentItem
        key={comment.title}
        title={comment.title}
        content={comment.content}
        likes={comment.likes}
        onClick={() => console.log('눌림')}   // 동작 함수 추가
        //or onClick={onSubmit}   
      />
      )}
    </div>
  )
}

왜...왜..왜일까?

memoization을 말할때 메모하는 대상은 props가 동일한 상태일때라고 했다.

하지만 위의 코드를 보면 CommentItem안에 인라인으로 함수( onClick )를 만들어 주기때문에

인라인으로 함수를 주면 이 함수가 새로 렌더링 될 때마다 새로운 함수가 만들어지는 것이다.

이것 때문에 렌더링 할때마다 CommentItem의 props중 함수의주소 값이 계속해서 바뀌기 때문에 props가 동일하지 않은 상태로 인식해 memo를 쓸 수 가 없었던것

그렇다면 아래처럼 함수 표현식을 통해 변수를 사용하여 사용하면 어떨까??

import React, { useCallback } from 'react'
import CommentItem from './CommentItem'

export default function Comments({ commentList }) {
  const handleChange = (() => {
    console.log('눌림')
  }, []);
  return (
    <div>
      {commentList.map((comment) => <CommentItem
        key={comment.title}
        title={comment.title}
        content={comment.content}
        likes={comment.likes}
        onClick={handleChange}
      />
      )}
    </div>
  )
}

여전히 똑같다.

이유인즉 Comments 컴포넌트 자체가 리렌더링 되기 때문이다
( props로 받은 commentList가 하나씩 추가되며 계속 바뀌기때문에 )

(Comments 컴포넌트가 리렌더링 되면 그때마다 CommentItem도 계속해서 리렌더링됨 )




🏷️ 2. useCallback 함수

useCallback(( ) => { }, [ ] )

이때 props를 통해 전달할 함수마저 memo할 때 사용하는 것이 useCallback 함수이다

위와 같이 리액트의 useCallback 함수를 이용하면 문제가 해결되어

import React, { useCallback } from 'react'
import CommentItem from './CommentItem'

export default function Comments({ commentList }) {
  const handleChange = useCallback(() => {
    console.log('눌림')
  }, []);
  return (
    <div>
      {commentList.map((comment) => <CommentItem
        key={comment.title}
        title={comment.title}
        content={comment.content}
        likes={comment.likes}
        onClick={handleChange}
      />
      )}
    </div>
  )
}

CommentItem에 전달하는 onClick={handleChange} 변수가
useCallback에 의해 전달할 함수또한 또한 메모이제이션 되었기 때문에
이전에 렌더링되었던 요소들이 다시 렌더링되지 않는다,
주소값이 다른 함수가 만들어지지 않게 막아주기때문.



🏷️ 3. useMemo

useMemo(() => { }, [ ])

useCallback()과 useMemo()는 dependency 배열 내부의 값으로 변경사항을 확인한다.

useMemo는 함수의 리턴 값을 메모이제이션 한다.

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

function CommentItem({title, content, likes, onClick}) {
  const [clickCount, setClickCount] = useState(0);

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

  const handleClick = () => {
    onClick()
    setClickCount(prev => prev + 1)
    alert(`${title} 눌림`)
  }

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

  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)

setClickCount함수를 통해 클릭 횟수를 파악하는 로직에서

클릭할때마다 rate()함수를 통해 효율성을 체크한 컴포넌트에 대해 다시 효율성을 체크하는 현상이 발생한다

이미 효율성 체크를 통과한 컴포넌트의 재 확인하는 함수인 rate를 어떻게 막을 수 있을까?

이때 사용하는것이 useMemo 이다.

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

function CommentItem({title, content, likes, onClick}) {
  const [clickCount, setClickCount] = useState(0);

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

  const handleClick = () => {
    onClick()
    setClickCount(prev => prev + 1)
    alert(`${title} 눌림`)
  }

  const rate = useMemo(() => {
    console.log("rate check");
    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)

아래 그림과 같이 눌림 콘솔은 뜨지만 ratecheck는 다시하지 않는다!!

const rate = useMemo(() => {
    console.log("rate check");
    return likes > 10 ? "Good" : "Bad";
  }, [likes])

클릭카운터는 바꼈어도 의존성으로 준 likes는 바뀌지 않았기때문이다. 위 코드에서 [ likes ] 변수로 주는 부분이

의존성부분이다, likes의 상태가 변하지 않으면 다시 실행하지 않는다.

useState()함수와 사용법이 비슷하다.

컴포넌트 메모이제이션 React.memo()
특정한 값을 메모이제이션 useMemo
특정한 함수를 메모이제이션 useCallback




2️⃣ 결론

🐸 : 하지만 결국 메모이제이션은 값을 캐싱하기 때문에 메모리 사용량이 증가할 수 있어요
메모리 사용 증가, 캐싱 오버헤드 등 오히려 예상치 못한 문제를 일으킬 수도 있기때문이죠

정리하자면 메모이제이션은 모든 상황에서 유용하지 않으며, 성능상의 이점을 실제로 얻을 수 있는지 판단해야 합니다. 재렌더링이 성능에 큰 영향을 미치는 경우에만 메모이제이션을 고려해보는게 중요해요!.

끝🙋🏻

💬 : 위 내용에대해 더 궁금한점이 있거나,
틀린내용이 있거나
질문이 있다면 댓글로 달아주세요.!

profile
Looking for an answer to what is a developer🧐;

0개의 댓글