왜 최적화 React.memo useMemo useCallback 비교표 📝 문제풀이
◀ 이전 📋 목차 CH 13 ▶
CHAPTER 12 · 고급

불필요한 리렌더를 줄이는 성능 최적화

리액트는 대부분 알아서 빠르게 그려줘요. 하지만 무거운 계산이나 아주 큰 리스트에서는 불필요한 리렌더가 느림을 만들 수 있어요. 이번 장은 그럴 때 꺼내 쓰는 세 가지 도구 — React.memo · useMemo · useCallback을 배웁니다.

🎯 이 장을 끝내면
🧩
이 장에서 뭐가 JS고 뭐가 리액트? 🟦 순수 JS: 값을 계산·비교하는 로직, 두 값이 ===로 같은지(참조 동일성), 함수·객체·배열이 새로 만들어졌는지 판단하는 것. 🟪 리액트: React.memo·useMemo·useCallback·리렌더링·의존성 배열. 값을 계산·판단하면 JS, "화면·컴포넌트에 어떻게 반영되나"는 리액트라고 보면 대체로 맞아요.
🤔
왜 최적화
그런데, 최적화가 왜 필요할까?
먼저 "언제 느려지는지"부터 이해해야 도구가 왜 필요한지 보여요.

리액트의 규칙은 단순해요. state(또는 props)가 바뀌면 그 컴포넌트가 다시 렌더되고, 그 자식 컴포넌트들도 함께 다시 렌더돼요. 여기서 "렌더"란 컴포넌트 함수가 다시 실행되어 새 가상 DOM을 만드는 것을 말해요. 실제 화면은 달라진 부분만 갱신되지만, 함수가 다시 실행되는 것 자체는 매번 일어납니다.

🖱️
state 변경
버튼 클릭 등으로 값이 바뀜
🔄
그 컴포넌트 리렌더
함수가 다시 실행됨
👨‍👧
자식들도 리렌더
아래로 쭉 함께 다시 그림
🔤
가상 DOM — 리액트가 실제 화면을 바꾸기 전에 메모리 속에 만들어 두는 화면의 설계도(사본)예요. 새로 만든 설계도와 이전 것을 비교해 달라진 부분만 실제 화면에 반영하죠. 그래서 "리렌더"가 곧 화면 전체를 새로 그리는 건 아니에요.

대부분의 화면에서는 이게 전혀 문제가 안 돼요. 리액트는 아주 빠르거든요. 그런데 이런 상황에서는 이야기가 달라져요.

🐢 리렌더가 느림을 만드는 두 경우🟦 순수 JS🟪 리액트 ① 무거운 계산 — 렌더할 때마다 수만 번 반복하는 계산이나 큰 배열 정렬 같은 걸 다시 돌리면, 매 렌더가 버벅여요.
② 아주 큰 리스트 — 수천 개의 자식 컴포넌트가 있는데, 부모가 리렌더될 때마다 자식 전부가 불필요하게 다시 렌더되면 느려져요. 즉, 문제는 "리렌더" 자체가 아니라 "불필요한(결과가 똑같은데도 다시 하는) 리렌더""매번 반복하는 무거운 계산"이에요.
⚠️
가장 중요한 원칙 — 미리 최적화하지 마세요. "혹시 느릴까 봐" 모든 곳에 memo를 붙이면 코드만 복잡해지고 오히려 손해예요. 먼저 평범하게 만들고, 실제로 느린 게 확인됐을 때(측정 후) 필요한 곳에만 이 도구들을 씁니다. 이걸 "필요할 때 최적화(optimize when needed)"라고 해요.
리렌더 state 변경 자식도 리렌더 React.memo useMemo useCallback 메모이제이션 의존성 배열 필요할 때만
🧊
React.memo
props가 같으면 리렌더 건너뛰기
컴포넌트를 감싸서, "받은 props가 그대로면 다시 그리지 마"라고 말해줘요.

React.memo컴포넌트를 감싸는 함수예요. 이렇게 감싸면, 부모가 리렌더되더라도 이 컴포넌트가 받는 props가 이전과 똑같으면 리렌더를 건너뜁니다. (이전에 그린 결과를 그대로 재사용해요.)

아래 예제에서 부모의 count가 바뀌어 부모가 리렌더돼도, Child가 받는 name props가 그대로면 Child는 다시 렌더되지 않아요.

import { useState, memo } from 'react';

// memo로 감싼 자식 — props(name)가 같으면 리렌더를 건너뜀
const Child = memo(function Child({ name }) {
  console.log('Child 렌더!');   // props가 바뀔 때만 찍힘
  return <p>안녕, {name}</p>;
});

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        count: {count}
      </button>

      {/* count가 바뀌어 Parent는 리렌더되지만,
          name은 그대로라 Child는 렌더를 건너뜀 */}
      <Child name="홍길동" />
    </div>
  );
}
🧭
어떻게 "같은지" 판단하나요?🟦 순수 JS React.memo는 이전 props와 새 props를 얕게 비교(shallow compare)해요. 즉 각 props 값을 ===로 비교합니다. 문자열·숫자처럼 값이 같으면 통과지만, 객체·배열·함수는 "새로 만들어지면 다른 것"으로 봐요. (이게 뒤의 useCallback이 필요한 이유예요.)
🧊 한 줄 정리🟪 리액트 React.memo(컴포넌트) = "props가 이전과 같으면 이 컴포넌트만 리렌더를 건너뛰어라." 큰 리스트의 각 항목처럼 같은 props로 자주 다시 그려지는 자식에 유용해요.
🧮
useMemo
비용 큰 "계산 결과(값)"를 기억하기
의존성이 그대로면 다시 계산하지 않고, 저번 결과를 꺼내 써요.

useMemo비용이 큰 계산의 결과값을 기억(메모이제이션)하는 훅이에요. 의존성 배열에 넣은 값이 이전과 같으면, 계산 함수를 다시 실행하지 않고 지난번에 기억해 둔 결과를 그대로 돌려줘요.

🔤
메모이제이션 — 한 번 계산한 결과를 기억해 두었다가, 같은 입력이면 다시 계산하지 않고 그대로 꺼내 쓰는 기법이에요. "메모(기록)해 둔다"고 생각하면 쉬워요. 그리고 use~로 시작하는 리액트의 특별한 기능 함수를 말해요.
import { useState, useMemo } from 'react';

function heavyCalc(a) {
  // 수만 번 반복하는 무거운 계산이라고 가정
  let total = 0;
  for (let i = 0; i < 100000000; i++) total += a;
  return total;
}

function Demo({ a }) {
  const [count, setCount] = useState(0);

  // a가 바뀔 때만 heavyCalc를 다시 실행,
  // count만 바뀌면 지난 결과를 그대로 재사용
  const result = useMemo(() => heavyCalc(a), [a]);

  return (
    <div>
      <p>결과: {result}</p>
      <button onClick={() => setCount(count + 1)}>
        count: {count}
      </button>
    </div>
  );
}

위에서 버튼을 눌러 count만 바뀌면 Demo는 리렌더되지만, useMemo의 의존성 [a]가 그대로라서 heavyCalc는 다시 돌지 않아요. a가 바뀔 때만 다시 계산합니다.

💡
의존성 배열이 핵심. [a]는 "a가 바뀔 때만 다시 계산해"라는 뜻이에요. []이면 처음 한 번만 계산하고 계속 기억하고, 배열을 빼면(생략) 매 렌더마다 계산해서 memo 효과가 사라져요. useEffect의 의존성 배열과 똑같은 규칙이에요.
🧮 언제 쓰나🟪 리액트 정말 무거운 계산일 때만 쓰세요. 간단한 계산(더하기, 짧은 map)에 useMemo를 붙이면, 기억하는 비용이 계산 비용보다 커져 오히려 손해예요.
🔗
useCallback
"함수"를 기억하기
매 렌더마다 새로 만들어지는 함수 때문에 생기는 불필요한 리렌더를 막아요.

문제 상황부터 볼게요. 컴포넌트가 렌더될 때마다 그 안에서 정의한 함수는 매번 "새로운 함수"로 다시 만들어져요. 겉보기엔 똑같아 보여도, 자바스크립트에게 () => {}는 만들 때마다 서로 다른 객체거든요.

그래서 React.memo로 감싼 자식에게 함수를 props로 넘기면, 부모가 리렌더될 때마다 그 함수가 새로 만들어져 "props가 바뀐 것"으로 인식돼요. 결국 memo가 소용없이 자식이 매번 리렌더됩니다.

useCallback함수 자체를 기억해서 이 문제를 풀어요. 의존성이 그대로면 지난번과 똑같은 함수(같은 참조)를 돌려주므로, memo 자식이 "props가 안 바뀌었네" 하고 리렌더를 건너뛸 수 있어요.

🔤
참조(reference) · 참조 동일성 — 객체·배열·함수는 값 자체가 아니라 "메모리 속 위치를 가리키는 주소"로 비교돼요. 그래서 내용이 똑같아도 새로 만들면 주소(참조)가 달라 ==="다른 것"이 되고, 같은 주소를 그대로 쓰면 "같은 것"이 돼요. 이 "주소가 같은가"를 참조 동일성이라고 해요. useCallback같은 참조를 유지해 주면 React.memo가 "안 바뀌었네" 하고 리렌더를 건너뛸 수 있는 거예요.
useCallback 없이
부모가 리렌더될 때마다 함수가 새로 만들어짐 → memo 자식이 "props 바뀌었네" 하고 매번 다시 렌더
useCallback 사용
의존성이 같으면 같은 함수(참조)를 유지 → memo 자식이 "안 바뀌었네" 하고 리렌더 건너뜀
import { useState, useCallback, memo } from 'react';

const Button = memo(function Button({ onClick }) {
  console.log('Button 렌더!');
  return <button onClick={onClick}>클릭</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // useCallback 없이 () => {...} 를 넘기면
  // 매 렌더마다 새 함수라 memo가 소용없음.
  // useCallback으로 함수를 기억 → 참조가 유지됨
  const handleClick = useCallback(() => {
    console.log('클릭됨!');
  }, []);   // 의존성 []: 항상 같은 함수 유지

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Button onClick={handleClick} />
    </div>
  );
}

이제 +1 버튼을 눌러 count가 바뀌어도, handleClickuseCallback 덕분에 같은 함수를 유지하므로 Button은 리렌더되지 않아요.

🧭
useCallback은 memo와 짝으로 쓸 때 의미가 커요. 자식이 React.memo로 감싸여 있지 않다면, 함수를 useCallback으로 기억해도 자식은 어차피 부모 따라 리렌더돼요. 그래서 보통 "memo 자식 + 함수 props" 조합에서 useCallback을 씁니다.
💡
useCallback(fn, deps)는 useMemo(() => fn, deps)와 같아요. useCallback은 "함수를 기억하는" 전용 축약형이라고 보면 돼요. deps가 같으면 같은 함수를, 바뀌면 새 함수를 돌려줍니다.
🆚
비교표
useMemo vs useCallback, 헷갈리지 않기
둘은 사실 아주 닮았어요. "무엇을 기억하느냐"만 다릅니다.
구분useMemouseCallback
기억하는 것 (계산 결과)함수 자체
돌려주는 것계산 함수를 실행한 결과값실행하지 않은 함수 그대로
대표 형태useMemo(() => calc(a), [a])useCallback(() => {...}, [a])
주로 언제비용 큰 계산 결과 재사용memo 자식에 함수 props 넘길 때
의존성 배열둘 다 배열의 값이 바뀔 때만 다시 만듦 (같으면 지난 것 재사용)
🧮
useMemo
값을 기억
/
🔗
useCallback
함수를 기억
/
🧊
React.memo
컴포넌트 리렌더 건너뜀
⚠️
남용하면 오히려 복잡하고 손해예요. 세 도구 모두 "기억"하는 데 메모리와 비교 비용이 들어요. 대부분의 컴포넌트는 그냥 두는 게 더 빠르고 읽기 쉽습니다. 실제로 느린 곳을 측정으로 확인한 뒤, 그 지점에만 골라 쓰세요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 코드)을 섞어 두었어요.
CH 13 🧭 라우팅 — React Router로 여러 페이지처럼 화면 전환하기