[250711] TIL

Today I Learned (2025-07-11)

리액트 렌더링 동작 원리 - 메모이제이션과 관계 학습 및 발표 준비함

  • Commit Phase 가 실제 DOM 을 변경하니깐, 불필요한 Commit Phase 단계를 막는 것이 최적화 좋겠다.
  • 실제 예시를 보면서 최적화를 진행해보겠습니다.

실전 1단계: 함수 Prop 최적화 (useCallback & React.memo)

React 컴포넌트 최적화의 가장 흔한 시나리오는 바로 “불필요한 자식 컴포넌트 리렌더링”을 막는 것입니다.

import React, { useState } from "react";

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

  const handleChildClick = () => console.log("자식 클릭!");

  console.log("👩‍👦 부모 리렌더링!");

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>
        부모 카운터: {count}
      </button>
      <Child onClick={handleChildClick} />
    </div>
  );
}

function Child({ onClick }) {
  console.log("👶 자식 리렌더링... 왜?");
  const list = Array.from({ length: 1000 }, (_, i) => i);

  return (
    <div>
      {list.map((item) => (
        <button key={item} onClick={onClick}>
          {item}번 자식입니다.
        </button>
      ))}
    </div>
  );
}
👩‍👦 부모 리렌더링!
👶 자식 리렌더링... ?

문제점

  • Child 컴포넌트는 count 상태와 아무 관련이 없습니다.
  • 하지만 부모가 리렌더링될 때마다 자식도 함께 리렌더링되고 있습니다.

2. 첫 번째 시도: useCallback

이 문제를 해결하기 위해 함수의 참조를 고정시키는 useCallback을 사용해 보겠습니다.

import React, { useState, useCallback } from "react";

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

  const handleChildClick = useCallback(() => console.log("자식 클릭!"), []);

  console.log("👩‍👦 부모 리렌더링!");

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>
        부모 카운터: {count}
      </button>
      <Child onClick={handleChildClick} />
    </div>
  );
}

function Child({ onClick }) {
  console.log("👶 자식 리렌더링... 왜?");
  const list = Array.from({ length: 1000 }, (_, i) => i);

  return (
    <div>
      {list.map((item) => (
        <button key={item} onClick={onClick}>
          {item}번 자식입니다.
        </button>
      ))}
    </div>
  );
}
👩‍👦 부모 리렌더링!
👶 자식 리렌더링... ?

왜 useCallback을 썼는데도 자식의 console.log가 계속 찍힐까요?

→ Commit Phase는 막았으나, Render Phase는 막지 못했다

useCallback은 함수의 참조 동일성을 보장해주는 것이 맞으나 이것만으로는 자식의 Render Phase 실행 자체를 막을 수 없음

// Babel 변환 후 (일부)
function Parent() {
  // ...
  const handleChildClick = React.useCallback(/* ... */);

  return React.createElement(
    "div",
    null,
    /* ... */
    React.createElement(Child, { onClick: handleChildClick })
  );
}
  • useCallback: handleChildClick 변수에는 항상 동일한 함수 참조가 담기도록 보장합니다.
  • Render Phase:
    • 실행 됨!
    • 부모가 리렌더링되면 Child를 위한 React.createElement가 호출되면서 자식의 Render Phase가 시작됩니다. (Child 함수가 실행되고 console.log가 찍힙니다.)
  • Commit Phase
    • 실행 안됨 !
    • Render Phase의 Diffing 과정에서 React는 Child의 onClick prop이 이전과 같다는 것을 useCallback 덕분에 알게 됩니다. 따라서 “실제 DOM 변경은 필요 없네”라고 판단하고 Commit Phase는 건너뛰게 됩니다.

useCallback만으로는 Render Phase 실행을 막지 못하고, 불필요한 Commit Phase만 막아주는 “반쪽짜리” 최적화가 됩니다. 🥲🥲

3. 진짜 해결책: React.memo로 Render Phase 차단하기

import React, { useState, useCallback } from "react";

const MemoizedChild = React.memo(function Child({ onClick }) {
  console.log("👶 자식 리렌더링 안됨! (성공)");
  const list = Array.from({ length: 1000 }, (_, i) => i);

  return (
    <div>
      {list.map((item) => (
        <button key={item} onClick={onClick}>
          {item}번 자식입니다.
        </button>
      ))}
    </div>
  );
});

function Parent() {
  const [count, setCount] = useState(0);
  const handleChildClick = useCallback(() => console.log("자식 클릭!"), []);

  console.log("👩‍👦 부모 리렌더링!");

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>
        부모 카운터: {count}
      </button>
      <MemoizedChild onClick={handleChildClick} />
    </div>
  );
}
👩‍👦 부모 리렌더링!
# (자식 로그는  이상 찍히지 않음)

동작 원리:

  1. Parent가 리렌더링됩니다. handleChildClick은 useCallback 덕분에 동일한 참조를 유지합니다.
  2. MemoizedChild는 렌더링되기 전, 전달받은 { onClick: handleChildClick } prop을 이전 prop과 비교합니다.
  3. handleChildClick의 참조가 동일하므로, MemoizedChild는 “Props에 변경이 없네!”라고 판단합니다.
  4. MemoizedChild의 Render Phase 실행 자체를 건너뜁니다.

useCallback + useMemo로 Render Phase, Commit Phase 렌더링을 막을 수 있다.


만약 자식에게 넘겨주는 Props가 객체라면 ?

// Parent 컴포넌트 내부
const childStyle = useMemo(() => ({ color: "blue" }), []);

const MemoizedChild = React.memo(function Child({ style }) {
  // ...
});
  • useCallback 대신 useMemo 를 사용하면 된다.

메모이제이션을 모든 곳에 사용하면 좋을까?

  • 메모리제이션은 공짜가 아님
  • 메모리 비용
  • 비교 비용

정말 필요할 때 적절하게 사용하자!

Categories:

Updated:

Leave a comment