React

[리액트] memoization, useMemo, useCallback

ejunyang 2024. 5. 29. 20:55

 

memoization❓

함수의 결과를 캐싱하여 동일한 입력에 대해 다시 계산하지 않도록 하는 최적화 기법이다. 이전의 계산한 값을 메모리에 저장하고, 중복된 계산을 제외하고 실행속도를 빠르게 해주는 기법이다.

 

캐싱 : 자주 사용하는 데이터를 임시로 저장해 두고, 필요할 때 빠르게 접근할 수 있도록 하는 것

 

 


 

 

 

 

React.memo

컴포넌트를 캐싱해서 부모 컴포넌트 리렌더링 시 캐싱된 컴포넌트는 리렌더링하지 않고 재사용하는 API이다.

이때 props를 받는 컴포넌트는 자식 컴포넌트이다. 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트는 props 상태가 변하지 않더라도 리렌더링된다. 불필요한 리렌더링을 막기 위한 방법을 해결하기 위해 React.memo를 사용하는 것이다.

 

아래 코드를 예시로 보자면 memo 를 사용하지 않았을 때는 input창에 값을 입력할 때마다 console.log("List component re-rendered"); 이 실행된다. 이는 부모컴포넌트에서 일어난 상태 변화에 자식 컴포넌트는 변화가 없음에도 불구하고 계속해서 리렌더링된다는 뜻이다. 

import { useState } from "react";

const List = (({ items }) => {
  console.log("List component re-rendered");
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
});

const App = () => {
  const [text, setText] = useState("");
  const [items, setItems] = useState([]);

  const handleAdd = () => {
    setItems([...items, text]);
    setText("");
  };

  return (
    <div>
      <input
	      type="text"
	      value={text}
	      onChange={e => setText(e.target.value)}
	      placeholder="Add item"
	    />
      <button onClick={handleAdd}>Add</button>
      <List items={items} />
    </div>
  );
};

export default App;

 

처음 렌더링된 후 콘솔이 한번 찍히고 입력창에 값을 입력할 때마다 리렌더링이 된다는 것을 확인할 수 있다. 

 

 

 

이때 React.memo를 사용해보면 차이를 확인할 수 있다. memo의 인자로 List라는 함수 컴포넌트를 넣었을 때 이전에 저장한 값을 불러와 리렌더링하지 않고 재사용할 수 있다.

import { useState, memo } from "react";

const List = memo(({ items }) => {
  console.log("List component re-rendered");
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
});

 

처음 렌더링될 때만 실행되고 입력창에 값을 넣어도 리렌더링되지 않는 것을 확인할 수 있다. 

 

 

 

 

 

 

useMemo

계산 결과 값을 캐싱해서 리렌더링 시 재사용할 수 있는 훅이다. 

useMemo는 자주 쓰이는 캐싱해준다. 그리고 그 값이 필요할때 다시 계산을 하는 것이 아닌 useMemo를 통해 캐싱을 한 값을 메모리에서 꺼내와서 재사용한다.

 

 

기본문법

아래 코드와 같이 useMemo는 인자로 함수와 의존 값(dependencies)를 받는다.

const cachedValue = useMemo(calculateValue, dependencies)

 

 

매개변수

calculateValue 캐시하려는 값을 계산하는 함수. 인자를 받지 않고, 반드시 어떤 타입이든 값을 반환해야한다. 의존성이 이전 렌더링 이후 변경되지 않았다면 동일한 값을 반환합니다. 그렇지 않으면 calculateValue를 호출하고 그 결과를 반환하며, 나중에 재사용할 수 있도록 저장한다.
dependencies React는 마지막 렌더링 이후에 의존 인자 중 하나의 값이라도 변경되면 calculateValue를 호출하여 값을 재계산한 리턴값을 반환한다. 의존 인자가 변경되지 않은 경우에는 동일한 값을 다시 반환한다.

 

 

 

아래 예시를 보면 컴포넌트가 렌더링될 때마다 value의 값이 초기화 된다. 렌더링이 될 때마다 불필요하게 재호출된다는 뜻이다. 이를 해결하기 위해 useMemo를 사용해보자.

function Component() {
    const value = calculate();
    return <div>{value}</div> 
}

function calculate() {
    return 10;
}

 

 

useMemo를 사용한 코드이다. useMemo도 useEffect처럼 첫번째 인자에 콜백함수, 두번째 인자엔 의존 인자를 가지고 있다. 의존 인자의 값이 변경될 때 콜백함수를 실행하여 값을 업데이트해준다. 만약 빈 배열을 넣는다면 useEffect와 마찬가지로 마운트 될 때에만 값을 계산하고 그 이후론 계속 캐싱된 값을 꺼내와 사용한다.

function Component() {
    const value = useMemo(() => {
    	return calculate();
    },[item])
    
    return <div>{value}</div> 
}

 

 

 

 

useCallback

함수 정의를 캐싱 해서 리렌더링 시 재사용할 수 있는 훅이다.

 

기본문법

const cachedFn = useCallback(fn, dependencies)

 

 

매개변수

fn 캐싱할 함숫값. 이 함수는 어떤 인자나 반환값도 가질 수 있다
dependencies fn 내에서 참조되는 모든 반응형 값의 목록. 반응형 값은 props와 state, 그리고 컴포넌트 안에서 직접 선언된 모든 변수와 함수를 포함

 

 

 

아래 코드는 컴포넌트가 렌더링할 때마다 handleIncrement 함수를 재생성한다. 렌더링될 때마다 같은 동작을 하지만, 함수의 참조가 변경되므로 React는 매 렌더링 시마다 새로운 함수로 인식하게된다. 이때 useCallback 훅을 사용해서 함수가 재생성되는 것을 해결할 수 있다.

import React, { useCallback, useState } from 'react';

const MyComponent = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount(prevCount => prevCount + 1);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

 

 

첫번째 인자에는 콜백함수를 넣고, 두번째 인자인 의존성 배열에는 count를 넣어 해당 값이 변경될 때마다 실행하도록 한다. 이렇게하면 렌더링될 때마다 함수가 재생성되는 것을 해결할 수 있으며, count 값이 변경되기 전 까진 이전에 캐싱한 함수를 재사용할 수 있다.

import React, { useCallback, useState } from 'react';

const MyComponent = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};