memo, useCallback, useMemo

고민경·2024년 7월 1일
import { useState } from 'react';

import Counter from './components/Counter/Counter.jsx';
import Header from './components/Header.jsx';
import { log } from './log.js';

function App() {
  log('<App /> rendered');

  const [enteredNumber, setEnteredNumber] = useState(0);
  const [chosenCount, setChosenCount] = useState(0);

  function handleChange(event) {
    setEnteredNumber(+event.target.value);
  }

  function handleSetClick() {
    setChosenCount(enteredNumber);
    setEnteredNumber(0);
  }

  return (
    <>
      <Header />
      <main>
        <section id="configure-counter">
          <h2>Set Counter</h2>
          <input type="number" onChange={handleChange} value={enteredNumber} />
          <button onClick={handleSetClick}>Set</button>
        </section>
        <Counter initialCount={chosenCount} />
      </main>
    </>
  );
}

export default App;

여기서 input에 문자 하나를 입력할 때마다 여러 컴포넌트 함수들이 다시 실행된다. 문자를 입력할 때마다 상태가 업데이트되기 때문에 컴포넌트 함수가 재실행된다.
이 문제를 고치기 위해서는 memo()를 사용할 수 있다.

memo()

컴포넌트 함수의 속성을 살펴보고 컴포넌트 함수가 정상적으로 다시 실행될 때, 예를 들어 앱 컴포넌트 함수가 실행되면 memo가 이전 속성 값과 새로 받을 속성 값을 살펴본다. 만약 컴포넌트 함수가 실행됐는데 속성 값들이 완전히 동일하다면 이 컴포넌트 함수 실행을 memo가 저지한다.

사용법은 아래와 같다.

import { useState, memo } from 'react';

import IconButton from '../UI/IconButton.jsx';
import MinusIcon from '../UI/Icons/MinusIcon.jsx';
import PlusIcon from '../UI/Icons/PlusIcon.jsx';
import CounterOutput from './CounterOutput.jsx';
import { log } from '../../log.js';

function isPrime(number) {
  log(
    'Calculating if is prime number',
    2,
    'other'
  );
  if (number <= 1) {
    return false;
  }

  const limit = Math.sqrt(number);

  for (let i = 2; i <= limit; i++) {
    if (number % i === 0) {
      return false;
    }
  }

  return true;
}

// memo import 해서 컴포넌트 감싸주기
const Counter = memo(function Counter({ initialCount }) {
  log('<Counter /> rendered', 1);
  const initialCountIsPrime = isPrime(initialCount);

  const [counter, setCounter] = useState(initialCount);

  function handleDecrement() {
    setCounter((prevCounter) => prevCounter - 1);
  }

  function handleIncrement() {
    setCounter((prevCounter) => prevCounter + 1);
  }

  return (
    <section className="counter">
      <p className="counter-info">
        The initial counter value was <strong>{initialCount}</strong>. It{' '}
        <strong>is {initialCountIsPrime ? 'a' : 'not a'}</strong> prime number.
      </p>
      <p>
        <IconButton icon={MinusIcon} onClick={handleDecrement}>
          Decrement
        </IconButton>
        <CounterOutput value={counter} />
        <IconButton icon={PlusIcon} onClick={handleIncrement}>
          Increment
        </IconButton>
      </p>
    </section>
  );
})

export default Counter;

여기서 initialCount가 바뀌거나 내부적 상태가 바뀌면 memo는 이를 저지하지 않으며, 컴포넌트 함수가 작동한다.

주의점

만약 memo로 모든 컴포넌트를 감싼다면 리액트는 컴포넌트 함수를 실행하기 전 항상 속성들을 확인해야 한다. 그리고 속성 값을 확인하는 건 그만큼 성능에 부담을 주게 된다. 그래서 최대한 상위 컴포넌트 트리에 있는 컴포넌트 또는 재렌더링을 방지할 수 있는 컴포넌트에 사용하는 것이 좋다.

useCallback()

useCallback 훅은 함수의 재생성을 방지하기 위해 사용된다.

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

import IconButton from '../UI/IconButton.jsx';
import MinusIcon from '../UI/Icons/MinusIcon.jsx';
import PlusIcon from '../UI/Icons/PlusIcon.jsx';
import CounterOutput from './CounterOutput.jsx';
import { log } from '../../log.js';

function isPrime(number) {
  log(
    'Calculating if is prime number',
    2,
    'other'
  );
  if (number <= 1) {
    return false;
  }

  const limit = Math.sqrt(number);

  for (let i = 2; i <= limit; i++) {
    if (number % i === 0) {
      return false;
    }
  }

  return true;
}

const Counter = memo(function Counter({ initialCount }) {
  log('<Counter /> rendered', 1);
  const initialCountIsPrime = isPrime(initialCount);

  const [counter, setCounter] = useState(initialCount);

  const handleDecrement = useCallback(function handleDecrement() {
    setCounter((prevCounter) => prevCounter - 1);
  }, []);

  const handleIncrement = useCallback(function handleIncrement() {
    setCounter((prevCounter) => prevCounter + 1);
  }, []);

  return (
    <section className="counter">
      <p className="counter-info">
        The initial counter value was <strong>{initialCount}</strong>. It{' '}
        <strong>is {initialCountIsPrime ? 'a' : 'not a'}</strong> prime number.
      </p>
      <p>
        <IconButton icon={MinusIcon} onClick={handleDecrement}>
          Decrement
        </IconButton>
        <CounterOutput value={counter} />
        <IconButton icon={PlusIcon} onClick={handleIncrement}>
          Increment
        </IconButton>
      </p>
    </section>
  );
})

export default Counter;

이렇게 하면 'Increment'버튼 또는 'Decrement' 버튼을 클리갷도 IconButton과 중첩 icon 컴포넌트들이 재실행되지 않으며 이제 counter output만 출력된다.

useMemo()

memo와 차이점: memo는 컴포넌트 함수를 감싸는데 사용했지만 useMemo는 컴포넌트 함수 안에 있는 일반 함수들을 감싸고 그들의 실행을 방지한다.


// 기존 코드
  const initialCountIsPrime = isPrime(initialCount);

// useMemo 사용
  const initialCountIsPrime = useMemo(() => isPrime(initialCount), [initialCount]);

isPrime의 불필요한 실행을 막기 위해 useMemo로 감싼 모습이다. useMemo는 첫 번째 인자로 콜백함수, 두 번째 인자로 의존성 배열을 받는다. 콜백 함수는 의존성 배열 안에 있는 값들이 바뀔 경우에만 재실행된다. 만약 빈 의존성 배열이 있다면 이것은 절대 재실행되지 않는다(바뀔 수 있는 의존성이 없기 때문).

0개의 댓글