dev.syw

불필요한 리렌더를 줄이는 memo·useMemo·useCallback.

성능 최적화

React는 충분히 빠르지만, 앱이 커지면 불필요한 리렌더가 쌓여 느려질 수 있습니다. 언제 최적화가 필요하고, 어떤 도구를 써야 하는지 알아봅시다.

학습 목표

  • 컴포넌트가 다시 렌더되는 원인을 안다.
  • React.memo 로 props가 같으면 리렌더를 건너뛸 수 있다.
  • useMemo 로 비싼 계산을, useCallback 으로 함수를 기억할 수 있다.
  • 최적화를 언제 써야 하고 언제 하지 말아야 하는지 판단할 수 있다.

리렌더는 왜 일어날까

컴포넌트는 다음 중 하나면 다시 렌더됩니다.

  • 자신의 state가 바뀔 때
  • 받는 props가 바뀔 때
  • 부모가 리렌더될 때 (props가 그대로여도 기본적으로 따라 렌더)

💡 TIP — 리렌더 자체는 나쁜 게 아닙니다. React가 가상 DOM을 비교해 실제 변경만 반영하니까요. "리렌더가 비싼 계산을 동반"하거나 "매우 자주 일어날 때"만 문제가 됩니다.

React.memo — props 같으면 건너뛰기

부모가 렌더돼도, props가 이전과 같으면 자식 렌더를 생략합니다.

import { memo } from 'react';

const Item = memo(function Item({ name }) {
  console.log('render', name);
  return <li>{name}</li>;
});

memo 는 props를 얕게 비교(===)합니다. 그래서 객체·배열·함수를 props로 넘기면, 매 렌더마다 새로 만들어진 참조 때문에 비교가 항상 "다름"이 됩니다. 여기서 useMemo·useCallback 이 필요해집니다.

useMemo — 비싼 계산 기억하기

의존성이 그대로면 이전 계산 결과를 재사용합니다.

import { useMemo } from 'react';

function ProductList({ products, keyword }) {
  const filtered = useMemo(
    () => products.filter((p) => p.name.includes(keyword)),
    [products, keyword] // 둘 다 그대로면 다시 계산 안 함
  );
  return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

useMemo 는 객체·배열의 참조 안정화에도 씁니다. memo 자식에 넘길 객체를 감싸면 불필요한 리렌더를 막습니다.

useCallback — 함수 기억하기

useCallback 은 함수의 참조를 유지합니다. memo 된 자식에 콜백을 넘길 때 핵심입니다.

import { useCallback, useState } from 'react';

function List({ items }) {
  const [, setLog] = useState([]);

  // ❌ 매 렌더 새 함수 → memo된 자식이 매번 리렌더
  // const onSelect = (id) => setLog((l) => [...l, id]);

  // ✅ 참조 유지 → 자식 리렌더 방지
  const onSelect = useCallback((id) => {
    setLog((l) => [...l, id]);
  }, []);

  return items.map((it) => (
    <Item key={it.id} item={it} onSelect={onSelect} />
  ));
}

useCallback(fn, deps)useMemo(() => fn, deps) 와 같다고 보면 됩니다.

도구무엇을 기억주 용도
React.memo컴포넌트props 같으면 렌더 생략
useMemo값(계산 결과)비싼 계산·참조 안정화
useCallback함수함수 참조 안정화

언제 쓰고 언제 안 쓰나

이 도구들은 공짜가 아닙니다. 비교 비용과 메모리, 그리고 코드 복잡도가 늘어납니다.

쓰면 좋은 경우

  • 측정해 보니 실제로 느린 비싼 계산이 매 렌더 반복될 때.
  • memo 된 자식에게 객체·함수 props를 넘겨, 그 자식 리렌더가 비쌀 때.
  • 리스트가 크고 자주 갱신될 때.

굳이 안 써도 되는 경우

  • 계산이 가볍고(작은 배열 map 등) 렌더가 드물 때.
  • memo 로 감싸지 않은 평범한 자식에 함수를 넘길 때 — useCallback 만 써봐야 효과 없음.

⚠️ 주의 — "혹시 몰라서" 모든 곳에 useMemo·useCallback 을 두르는 건 안티패턴입니다. 먼저 React DevTools Profiler로 측정하고, 병목이 확인된 곳만 최적화하세요.

💡 TIP — React Compiler(React 19와 함께 발전 중)를 쓰면 이런 메모이제이션을 자동으로 처리해 줍니다. 수동 최적화 전에 도구의 도움을 받을 수 있는지 살펴보세요.

요약

  • 리렌더는 state·props 변경, 부모 렌더로 일어난다. 리렌더 자체는 정상.
  • React.memo 는 props가 얕게 같으면 자식 렌더를 건너뛴다.
  • useMemo 는 값을, useCallback 은 함수를 기억해 참조를 안정화한다.
  • memo 자식에 객체·함수를 넘길 때 useMemo·useCallback 이 짝을 이룬다.
  • 먼저 측정하고, 병목이 있는 곳만 최적화한다.

연습문제

  1. console.log 를 넣어 부모 state가 바뀔 때 자식이 함께 리렌더되는지 확인하세요.
  2. 위 자식을 React.memo 로 감싸고, 관련 없는 props가 그대로일 때 렌더가 생략되는지 보세요.
  3. 큰 배열을 정렬·필터하는 계산을 useMemo 로 감싸 의존성이 같을 때 재계산이 없음을 확인하세요.
  4. memo 된 자식에 콜백을 넘길 때 useCallback 유무에 따른 리렌더 차이를 비교하세요.

힌트 — React DevTools의 "Highlight updates"나 Profiler로 리렌더를 눈으로 확인할 수 있습니다.

💡 연습문제 풀이

불러오는 중…

함께 보면 좋은 자료

댓글 0

React.js” 강좌에 대한 댓글입니다.

댓글을 작성하려면 로그인이 필요합니다.