React/[React] 메모이제이션

[React] useMemo, useCallback 사용법

kind1230 2024. 1. 1. 22:27

들어가며

이전 React.memo 사용법 글에서 리렌더링하는 대표적인 조건으로 props가 변경되었을때 react.memo를 사용는 방법을 살펴 보았다. 그러나 React 애플리케이션에서 성능 최적화를 위해 고려해야 할 요소는 이것만이 아니다. 이번 글에서는 React의 또 다른 두 가지 중요한 도구인 useMemouseCallback에 대해 알아보고 언제, 왜 사용하고 어떻게 성능향상에 기여할 수 있는지 알아보려고 한다.

 

1. useMemo

useMemo 공식문서를 확인해 보자.

"useMemo is a React Hook that lets you cache the result of a calculation between re-renders."
useMemo는 리렌더링 사이에 계산 결과를 캐시할 수 있는 React Hook입니다.

리액트는 상태가 업데이트 될 때마다 리렌더링이 되기 때문에 이전에 쓰이던 값과 똑같은 결과를 내는 복잡한 연산이 들어있는 함수(useCallback), 그 결과값(useMemo)들 까지도 새롭게 불러오는 것은 엄청난 낭비가 될 수 있다.
useMemo는 React의 훅(Hook) 중 하나로, 계산 비용이 많은 함수의 결과 값을 기억하고 재사용하는 데 사용된다. 이를 통해 성능을 최적화할 수 있다.

 

1-1. useMemo 활용예시

테스트 환경 - codepen

import React from 'https://esm.sh/react@18.2.0'
import ReactDOM from 'https://esm.sh/react-dom@18.2.0'

// 기본 컴포넌트
const MyComponent = () => {
  const [count, setCount] = React.useState(0);
  const [inputValue, setInputValue] = React.useState('');

  const calculateMemoizedCount = (count) => {
    // 복잡한 계산 로직 (예시로 for문 사용)
    let result = count;

    for (let i = 0; i < 3000; i++) {
      console.log('테스트 :: ',i);
      result += i;
    }
    console.log('끝');
    return result;
  };

  // count 값이 변경될 때만 함수를 실행하고 이전에 계산된 값을 재사용
  const memoizedCount = React.useMemo(() => calculateMemoizedCount(count), [count]); 

  return (
    <div>
      <p>Count: {count}</p>
      <p>Memo Count: {memoizedCount}</p>
      <input
        value={inputValue}
        onChange={(e) =>     setInputValue(e.target.value)}
        placeholder="Enter a value"
      />
      <button onClick={() => setCount(count + 1)}>count</button>
    </div>
  );
};
ReactDOM.render(<MyComponent/>,document.getElementById("root"));

위의 예시코드 에서는

  • count와 inputValue라는 두 개의 상태 값을 useState를 통해 관리한다.
  • memoizedCount는 useMemo를 사용하여 첫번째 인자값에 계산 결과를 반환하는 함수, 두번째 인자에 의존성 배열을 넣어 줍니다. 위 예시코드에서 의존성배열에 count가 들어가 있고 count가 변경되면, 첫 번째 인자로 전달된 함수가 실행되고 그 결과가 업데이트 된다. (위 예시코드에서는 button을 클릭을 할 때마다 업데이트 된다.)
  • inputValue는 useMemo의 의존성 배열에 관련이 없는 상태 값이며, inputValue가 업데이트가 되어도 count는 변하지 않기 때문에 캐시된 값을 가져와서 사용한다.

성능 비교 테스트

테스트 플로우
카운트 버튼 1번 클릭 -> input창에 두번 값(예: 1,2)을 입력.

 

useMemo 사용 전

 

 

useMemo 사용 후

 

 

위 예시코드를 개발자도구 Performance를 사용하여 테스트결과

  • useMemo 사용 전: 3번의 렌더링
  • useMemo 사용 후: 1번의 렌더링

Scripting, Rendering에서 1초정도 차이를 보이고 있다.

 

1-2. useMemo 언제 사용해야 될까?

  1. 과도한 계산이 포함된 함수인 경우 : 두번째 인자가 비어있을 경우 최초렌더링 한번만 계산되고 이후 저장된 값을 사용한다. 
  2. const memoizedValue = useMemo(() => { // 과도하고 거대한 로직... }, []);
  3. 불필요한 렌더링을 방지하고 성능을 최적화해야 하는 경우 : 위 예시코드처럼 관련없는 상태 값이 업데이트 되었을때 불필요한 렌더링을 방지한다.
  4. 참조값이 변경되지 않는 경우 : React.memo와 같이 사용하면 자식 컴포넌트에게 전달할 때 참조값이 변하지 않으면 불필요한 리렌더링을 방지한다.

 

2. useCallback

useCallback 공식문서를 확인해 보자.

"useCallback is a React Hook that lets you cache a function definition between re-renders."
useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있게 해주는 React Hook입니다.

useMemo에서도 말 했듯이, 리액트는 상태가 업데이트 될 때마다 리렌더링이 되기 때문에 이전에 쓰이던 값과 똑같은 결과를 내는 복잡한 연산이 들어있는 함수(useCallback), 그 결과값(useMemo)들 까지도 새롭게 불러오는 것은 엄청난 낭비가 될 수 있다고 설명 하였다. useCallback은 메모이제이션을 통해서 특정함수를 재사용한다는 것이다. useCallback과 useMemo의 차이점은 함수를 재사용하느냐, 값을 재사용하느냐의 차이다. useCallback 예시를 보기전에 함수동등성을 짧게 보고가자.

 

함수동등성이란?
자바스크립트에서 함수는 객체로 취급이 되기때문에, 함수를 동일하게 만들어도 메모리 주소가 다르면 다른 함수로 간주한다.

const add1 = () => x + y;
const add2 = () => x + y;
add1 === add2
false

 

이러한 자바스크립트의 특성은 React 컴포넌트 내에서 어떤 함수를 자식 컴포넌트의 props로 넘길 때 예상치 못한 성능 문제(불필요한 렌더링)로 이어질 수 있다.

 

2-1. useCallback 활용예시

아래와 같이 데이터를 가져오는 fetchData 함수를 만들고, useEffect에 의존성 배열로 fetchData를 추가해보자.

import React, { useState, useEffect } from 'react'

function Profile({ id }) {
  const [data, setData] = useState(null)

  const fetchData = () =>
    fetch(`https://test-api.com/data/${id}`)
      .then(response => response.json())
      .then(({ data }) => data)

  useEffect(() => {
    fetchData().then(data => setData(data))
  }, [fetchData])

  // ...
}
  1. 언뜻 보면 페이지가 렌더링이 되고 useEffect를 통하여 데이터 가져오는 fetchData 함수를 호출해 데이터를 잘 가져오는 듯 보인다.
  2. 위에서 설명한듯이 함수의 동등성 문제 때문에 예상치 못한 무한루프에 빠지게 된다.
  3. fetchData는 함수이기 때문에 id 값에 관계없이 컴포넌트가 렌더링 될 때마다 새로운 참조값으로 변경이 된 함수가 변경되었으므로, 매번 useEffect가 실행되어 다시 렌더링이 되고 무한루프에 빠지게 된다.

 

아래는 위 코드에 useCallback을 활용해 보겠다.

import React, { useState, useEffect } from 'react'

function Profile({ id }) {
  const [data, setData] = useState(null)

  const fetchData = useCallback(
    () =>
      fetch(`https://test-api.com/data/${id}`)
        .then(response => response.json())
        .then(({ data }) => data),
    [id],
  )

  useEffect(() => {
    fetchData().then(data => setData(data))
  }, [fetchData])

  // ...
}
  • 위 예시처럼 useCallback을 사용하여 메모이제이션 하면, 컴포넌트가 리렌더링이 되더라도 두번째 인자에 의존성배열[id]가 변하지 않으므로 fetchData 함수의 참조값을 동일하게 유지시킨다.
  • useEffect에 의존성 배열에 있는 fetchData함수는 props로 받는 id값이 변경되지 않는 한 재호출 되지 않는다.

 

2-3. useCallback 언제 사용해야 될까?

  • 위 예시처럼 무한 루프에 빠질 위험이 있을 경우.
  • 자식 컴포넌트에 함수를 props로 넘길 때, 불필요한 렌더링이 일어난다고 판단되는 경우.
  • 함수 자체가 매우 복잡하거나, 다시 계산하는데 비용이 많이 드는 경우.

 

3. useCallback, useMemo 주의사항

공식 문서를 참고해보면 useMemo의 설명 중에 굵은 글씨로 "useMemo는 성능 최적화를 위해서 사용될 수 있지만 의미상으로 보장이 있다고 생각하지는 마라." 라는 말이 있다. useMemo는 분명 성능 최적화를 해주고, 좀 더 웹, 앱을 빠르게 만들어 줄 수 있지만, 무분별하게 useMemo로 감싸게 되면 이 또한 리소스 낭비가 될 수 있으므로 퍼포먼스 최적화가 필요한 연상량이 많은 곳에 사용하는 것이 좋다.
useCallback도 마찬가지로 꼭 필요한 상황에 사용하라고 적혀있다.

  • 연산이 복잡하지 않은 함수일 경우
  • 의도적으로 매번 새로운 함수나 값을 계산해야 하는 경우
  • useCallback, useMemo의 의존성 배열에 완전히 새로운 객체나 배열을 전달해서는 안된다. 만약 useCallback 내부 함수나 useMemo 내부 값에서 사용하지 않는 props를 전달한다면 메모이제이션을 하는데 소용이 없다.
useMemo에 언제 사용해야 성능상 이점을 가져올 수 있는지 테스트한 글이다.
https://github.com/yeonjuan/dev-blog/blob/master/JavaScript/should-you-really-use-usememo.md

 

 

마무리

이번 글을 작성하면서 조금이라도 React Memoization에 대해 조금은 알게 된 것 같다.
성능최적화를 위해 메모이제이션을 하는데 대표적으로 React.memo, useMemo, useCallback가 있고,
모두 성능최적화를 하기한 훌륭한 도구이지만 무작정 써야되는게 아니라 언제 어떤 상황에서 알맞게 사용해야 될 지가 정말 중요하다는걸 알 수 있었고 무분별하게 사용하게 된다면 오히려 성능저하가 올 수 있다는 심각성도 알 수 있었다.

 

참고