[Hook] useMemo

OlMinJe·2025년 9월 2일

React

목록 보기
12/19

리액트 공식 문서를 참고한 정리 내용 (25.08 기준)

재렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 Hook이다.

const cachedValue = useMemo(calculateValue, dependencies)

컴포넌트의 최상위 레벨에 있는 useMemo를 호출하여 재렌더링 사이의 계산을 캐싱한다. 이게 무슨 말이냐면, 값 계산 결과를 메모이제이션해서 불필요한 재계산을 막아준다는 의미!

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

매개변수

calculateValue

  • 계산 함수이다.
  • 한 번 실행하면 어떠한 값을 만들어내며, 이 함수는 외부에 영향을 주면 안되고 단순히 입력값에서 결과값만 반환하는 순수 함수여야 한다.
  • 처음 렌더링할 때 한 번 실행되고, 이후에는 의존성이 바뀔 때만 다시 실행된다.

dependencies

  • 계산에 영향을 주는 변수의 목록으로, 이 배열에 저장한 값이 하나라도 바뀌면 계산 함수를 다시 실행한다.
  • 안 바뀌면 이전 결과를 그대로 재사용한다

의존성(dependencies)이 바뀌지 않는 한 이전 결과를 기억해 두었다가 재사용하게 해준다.


반환값

처음 렌더링할 때 인자 없이 계산 함수를 호출하여 결과를 반환한다.
다음 렌더링에서는, 의존성의 변경 여부에 따라 반환하는 값이 달라진다.

  • 의존성이 변경 X: 마지막 렌더링때 저장된 값을 반환gksek.
  • 의존성이 변경 O: calculatValue를 호출하여 결과를 저장하고 반환한다.

주의사항

1. 호출 위치

  • useMemo는 Hook이기 때문에 항상 컴포넌트의 맨 위나 자체 Hook에서만 사용해야 하며,
  • 분기문 혹은 조건문 내의 로직에 사용하고 싶은 경우에는 새로운 컴포넌트로 빼서 해당 로직에 넣는 방식으로 사용해야 한다.

2. Strict Mode

  • 개발 모드에서는 일부러 두 번 실행하니 너무 놀라지 않아도 된다

3. 캐시 버리는 경우
원칙적으로 React는 캐시(이전 계산 결과)를 오래 들고 있지만 몇 가지 상황에서는 버린다.

  • 컴포넌트 파일을 수정했을 때 (개발 중)
  • 컴포넌트가 처음 마운트될 때 “일시 중단”되었을 때
  • 앞으로 React가 제공할 새로운 기능들(예: 스크롤 최적화)에서도 버릴 수 있음

useMemo는 “있으면 성능 최적화” 도구지, 상태 관리 도구가 아니기 때문에 값을 실제로 보존하고 싶으면 stateref를 쓰는게 좋다

useMemo의 뜻을 다시 짚어보자!
우선 memoization(메모이제이션)을 알아해 한다.

📌 memoization (메모이제이션) 이란?

어떤 계산 결과를 기억해 두었다가 다음에 똑같은 입력이 들어오면 다시 계산하지 않고 기억해둔 결과를 바로 돌려주는 것을 의미한다.
= 계산을 메모해둠

📌 useMemo

  • use → 리액트 훅
  • Memo → memoization을 해서 값을 기억한다는 의미

사용법

비용이 높은 로직의 재계산 생략하기

const visibleTodos = useMemo(
  () => filterTodos(todos, tab), // 계산 함수
  [todos, tab]                   // 의존성 목록
);

컴포넌트가 다시 렌더링되더라도 todostab이 그대로면 이전 계산 결과를 그래도 사용한다. 만약 둘 중 하나라도 바뀌면 다시 계산을 진행!

왜 이렇게까지 하는 거예요..~?
보통은 계산이 금방 끝나서 괜찮지만, 큰 배열 필터링이나 복잡한 연산은 렌더링할 때마다 다시 계산하면 느려질 수 있다.
이럴 때 useMemo를 사용하면 의존성에 있는 데이터가 바뀌지 않은 한, 이전 계산값을 재사용해서 성능을 지킬 수 있다.

useMemo를 꼭 사용해야 하는지 판단하는 기준이 뭐꼬!

연산 비용 측정하기

console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');

console.time / console.timeEnd 를 쓰면, 함수 실행에 얼마나 시간이 걸렸는지 콘솔에 찍어주는데, 시간이 거의 0.1ms~1ms 이하면 사실상 빠른 거라서 useMemo로 굳이 감쌀 필요가 없다.
개발 환경(Strict Mode)에서는 React가 일부러 두 번 실행하기도 해서, 체감상 더 느려 보일 수도 있기 때문에, 개발자 도구(ex. 크롬 성능 분석)로 확인하는게 제일 확실하다.

useMemo로 억지로 최적화하기 전에 지켜야 할 기본 원칙

JSX로 자식 감싸기

넌트를 단순히 레이아웃(껍데기) 용도로 감쌀 때는 children을 사용하면, 부모가 다시 렌더링돼도 React는 자식을 그대로 두고 새로 안 그린다.

<Box>
  <TodoList /> {/* 부모 Box가 리렌더링돼도 TodoList는 다시 안 그림 */}
</Box>

지역 상태(local state) 우선

상태는 상태가 꼭 필요한 가까운 컴포넌트 안에 두는게 가장 좋다. 만약 불필요하게 상위 컴포넌트로 올려버리면, 상위 컴포넌트가 바뀔 때마다 그 아래에 있는 컴포넌트 전체가 다시 렌더링된다.

순수한 렌더링 로직 유직

컴포넌트는 Props로 UI 변환만 하도록 해야 한다. 즉, 컴포넌트 설계를 기깔나게 해야 한다.

불필요한 Effect 줄이기

성능 문제 대부분은 Effect가 상태를 계속 바꿔서 무한 리렌더링 되는 데서 생긴다. 그러니 useEffect로 상태를 업데이트할 필요가 없다면 그냥 쓰지 말자.

Effect 안의 종속성 최소화

Effect에서 객체/함수 같은 걸 의존성으로 두면 자꾸 새로 만들어져서 렌더링이 반복되는데, 그럴 땐 Effect 안에서 직접 정의하거나, 아예 컴포넌트 바깥으로 빼는 게 훨 나은 방법이다.


useMemo로 덮어놓고 최적화하려고 하기 전에, 상태 구조 단순화 + 불필요한 렌더링 원인 제거를 먼저 하는 게 진짜 성능 최적화라는 뜻입니당!


Effect가 자주 실행되지 않도록 하기

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const options = {
    serverUrl: 'https://localhost:1234',
    roomId: roomId
  }

   useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);
  // ...

options 객체를 종속성으로 사용하면 리렌더링될 때마다 새로운 객체가 만들어지기 때문에, 채팅방이 불필요하게 계속 재연결되는 문제가 발생한다.
이러한 문제는 아래의 코드처럼 useMemo로 감싸면 된다.

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const options = useMemo(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]);
  
  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);
  // ...

roomId가 안 바뀌면 options도 그대로 유지돼서, useEffect도 필요할 때만 실행된다.


다른 Hook의 종속성 메모화

객체와 배열은 매번 새로 만들어지기 때문에 의존성을 배열에 넣으면 useMemo가 무용지물이 될 수 있다.
이러한 문제를 해결하기 위해 useMemo로 그 객체 자체를 캐싱하거나 아예 useMemo 내부에 직접 만들면 된다

function Dropdown({ allItems, text }) {
  const searchOptions = { matchMode: 'whole-word', text };

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]);
}

// 해결 방법 1: useMemo로 searchOptions를 캐싱
const searchOptions = useMemo(() => {
  return { matchMode: 'whole-word', text };
}, [text]);

// 해결 방법 2: 아예 useMemo 안에서 만들기
const visibleItems = useMemo(() => {
  const searchOptions = { matchMode: 'whole-word', text };
  return searchItems(allItems, searchOptions);
}, [allItems, text]); // text 바뀔 때만 다시 실행된다.

useMemo가 객체를 반환해야 하는데 undefined를 반환합니다.

  const searchOptions = useMemo(() => {
    matchMode: 'whole-word',
    text: text
  }, [text]);

여기서 화살표 함수의 중괄호 {}객체 리터럴이 아니라 함수의 실행 블록이다. return이 없으니 당연히 undefined가 반환되는 것!

올바르게 결과를 반환하고 싶다면 객체 리터럴을 괄호로 감싸주면 된다.

 const searchOptions = useMemo(() => ({
    matchMode: 'whole-word',
    text: text
  }), [text]);

하지만 괄호로 감싸주는 방식은 가독성이 구리기 때문에 명시적으로 return을 사용하는 걸 추천한다고 하네..

 const searchOptions = useMemo(() => {
    return {
      matchMode: 'whole-word',
      text: text
    };
  }, [text]);

반복문에서 각 항목에 대해 useMemo를 호출하는 방법

아래와 같이 반복문(map) 안에서 직접 useMemo를 호출하면 Hook 호출 규칙을 위반하기 때문에 에러가 발생한다.

function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        const data = useMemo(() => calculateReport(item), [item]);
        return (
          <figure key={item.id}>
            <Chart data={data} />
          </figure>
        );
      })}
    </article>
  );
}

해결 방법 1: 컴포넌트 분리
반복문 안에서 Hook을 직접 호출하지 말고, 각 항목을 전담하는 컴포넌트를 따로 빼서 useMemo를 사용한다.

function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  const data = useMemo(() => calculateReport(item), [item]);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
}

이 방식은 useMemo가 컴포넌트의 최상위 레벨에서 호출되므로 Hook 규칙을 지킨다.

해결 방법 2: 컴포넌트를 memo로 감싸기
계산 로직을 useMemo로 최적화하는 대신, 컴포넌트 자체를 memo로 감싸서 props가 바뀌지 않으면 불필요한 렌더링을 막을 수도 있다.

const Report = memo(function Report({ item }) {
  const data = calculateReport(item);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
});

이 경우 item prop이 변경되지 않는 한 Report는 재렌더링되지 않는다.

profile
큐트걸

0개의 댓글