유명한 담벼락

React.memo, useCallback, useMemo 차이

by 담담이담

useMemo

  • 비용이 큰 연산 결과를 저장하고, 이 저장된 값을 반환하는 훅
  • 첫 인수로는 어떤 값을 반환하는 생성함수, 두 번째 인수로는 해당함수가 의존하는 값의 배열을 전달
  • 렌더링 시 의존성 배열의 값이 변경되지 않았으면, 함수를 재 실행하지 않고 기억해둔 해당 값을 반환
  • 의존성 배열의 값이 변경되었다면, 첫 인수의 함수를 실행하고 그 값을 반환한 다음 또 그 값을 기억
  • 메모이제이션은 값 뿐만 아니라 함수도 가능
  • 물론 컴포넌트도 useMemo로 감쌀 수 있지만, React.memo를 쓰는 것이 현명

useCallback

  • useMemo가 값을 기억했다면, useCallback은 인수로 넘겨받은 콜백 자체를 기억
  • 새로 특정함수를 만들지 않고, 재 사용한다는 말
  • 첫 번쨰 인수로 함수를 두 번째 인수로 의존성 배열을 집어넣음
  • 함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지하고 싶을 때 사용
  • useMemo와 useCallback의 유일한 차이는 메모이제이션을 하는 대상이 변수냐 함수냐일 뿐

 

예시로 살펴보기 

import React, { useReducer } from 'react';
import personReducer from './reducer/person-reducer';

export default function AppMentorsButton() {
  const [person, dispatch] = useReducer(personReducer, initialPerson);

  const handleUpdate = () => {
    const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
    const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
    dispatch({ type: 'updated', prev, current });
  };

  const handleAdd = () => {
    const name = prompt(`멘토의 이름은?`);
    const title = prompt(`멘토의 직함은?`);
    dispatch({ type: 'added', name, title });
  };

  const handleDelete = () => {
    const name = prompt(`누구를 삭제하고 싶은가요?`);
    dispatch({ type: 'deleted', name });
  };

  return (
    <div>
      <h1>
        {person.name}는 {person.title}
      </h1>
      <p>{person.name}의 멘토는:</p>
      <ul>
        {person.mentors.map((mentor, index) => (
          <li key={index}>
            {mentor.name} ({mentor.title})
          </li>
        ))}
      </ul>
      <Button text="멘토의 이름을 바꾸기" onClick={handleUpdate} />
      <Button text="멘토 추가하기" onClick={handleAdd} />
      <Button text="멘토 삭제하기" onClick={handleDelete} />
    </div>
  );
}

function Button({ text, onClick }) {
  console.log('Button', text, 're-rendering 😜');

  const result = calculateSomething();
  return (
    <button
      onClick={onClick}
      style={{
        backgroundColor: 'black',
        color: 'white',
        borderRadius: '20px',
        margin: '0.4rem',
      }}
    >
      {text} {result}
    </button>
  );
}

function calculateSomething() {
  for (let i = 0; i < 10000; i++) {
    console.log('🥹');
  }
  return 10;
}

const initialPerson = {
  name: '엘리',
  title: '개발자',
  mentors: [
    {
      name: '밥',
      title: '시니어개발자',
    },
    {
      name: '제임스',
      title: '시니어개발자',
    },
  ],
};

 

  • 멘토를 추가, 삭제, 변경할 때마다 Button 컴포넌트들이 리렌더링됨
  • 왜? 상위 컴포넌트인 AppMentorsButton의 state가 변경되기에 하위 컴포넌트인 Button도 리렌더링됨
  • 또한, AppMentorsButton가 리렌더링 되면서, handleUpdate, handleAdd, handleDelete라는 함수가 전부 다시 만들어짐
  • 뿐만 아니라, Button에 프롭으로 보내주는 text도 새로운 값으로 만들어짐
  • (변수에 새롭게 할당된 문자열이 text라는 prop으로 전달되는 것과 같음
  • 만약 Button이라는 컴포넌트 안에 다른 컴포넌트들이 존재한다면 그것들까지 다 재렌더링이 될 거임
  • 하지만 그렇게 걱정할 일은 아니긴 함
    • 왜? 리액트의 가상돔이 실제로 변경이 일어난 요소만 화면에 바뀌어서 보여지기 때문(렌더링 O, 커밋 X)
    • Button이 수많은 자식 컴포넌트를 가지고 있거나,
    • 무거운 계산이나 API 콜을 하는 게 아니라면 성능에 영향이 없음

 

하지만,

  • Button 컴포넌트 내에서 위처럼 for문을 돌며 무거운 연산을 해야한다면 버튼 하나를 리렌더링하는데에 시간이 오래걸릴 거임
  • 즉, 가상돔을 만들고 어떤 걸 업데이트 해야하는지 결정하는데까지도 시간이 걸려버림
  • 처음에만 계산해야한다면, useEffect를
  • 또는 useMemo를 사용하면 됨

 

function Button({ text, onClick }) {
  console.log('Button', text, 're-rendering 😜');

  const result = useMemo(() => calculateSomething());
  return (
    <button
      onClick={onClick}
      style={{
        backgroundColor: 'black',
        color: 'white',
        borderRadius: '20px',
        margin: '0.4rem',
      }}
    >
      {text} {result}
    </button>
  );
}
  • 하지만 이렇게 바꾸더라도, 상위 컴포넌트가 리랜더링 되면서 props로 들어오는 text와 onClick이 새롭게 만들어진 값으로 변경되기 때문에 Button이 리렌더링이 됨
  • <Button text="멘토의 이름을 바꾸기" onClick={handleUpdate} />
    • 이렇게 프롭을 전달하게 되면, 컴포넌트를 호출할 때마다 Props라는 새로운 객체가 만들어짐
    • 새로운 props라는 객체가 만들어지더라도, 이 안의 값이 동일한 값이라면 리렌더링 하지마! 라고 해주는게 memo임

 

const Button = memo(({ text, onClick }) => {
  console.log('Button', text, 're-rendering 😜');
  const result = useMemo(() => calculateSomething(), []);
  return (
    <button
      onClick={onClick}
      style={{
        backgroundColor: 'black',
        color: 'white',
        borderRadius: '20px',
        margin: '0.4rem',
      }}
    >
      {`${text} ${result}`}
    </button>
  );
});

Button.displayName = 'Button';
  • 여기서 질문..
    • 너가 text도 새로 만들어져서 들어온다며 그럼 걔도 메모이제이션 해줘야하는 거 아님? useCallback으로 onClick으로 들어올 함수만 메모이제이션 해주면 됨?
    • 음.. text는 메모이제이션 안해줘도 됨 근데 onClick으로 들어오는 함수는 메모이제이션 해줘야함
  • 왜?
  • memo 함수는 컴포넌트의 props가 변경되지 않으면 리렌더링이 되지 않게 막아줌
  • 하지만 props의 얕은 비교를 수행하므로, props 내부의 객체나 함수와 같이 참조가 변경되지 않으면 변경이 없다고 판단함
  • text는 문자열이므로 원시 데이터 타입으로 취급되며, onClick은 함수이기 때문에 참조가 변경되지 않는 한 리렌더링이 발생하지 않음 따라서 memo가 효과적으로 작동하여 Button의 불필요한 리렌더링을 방지함

블로그의 정보

유명한 담벼락

담담이담

활동하기