"왜 이 컴포넌트가 다시 렌더링됐지?" — 이 질문에 추측이 아닌 데이터로 답할 수 있을까요?

성능 최적화의 첫 단계는 측정 입니다. React DevTools의 Profiler는 컴포넌트별 렌더링 시간, 렌더링 원인, 불필요한 리렌더링을 시각적으로 보여줍니다. 감으로 최적화하는 것이 아니라, 데이터를 기반으로 정확한 병목을 찾는 방법을 정리합니다.

React DevTools 설치

Chrome이나 Firefox 확장 프로그램으로 설치합니다. 설치 후 DevTools에 "Components"와 "Profiler" 탭이 추가됩니다.

Profiler 기본 사용법

  1. DevTools에서 Profiler 탭을 선택합니다
  2. ** 녹화 버튼 **(파란 원)을 클릭합니다
  3. 앱에서 프로파일링할 동작을 수행합니다
  4. 녹화를 중지합니다
  5. 결과를 분석합니다

Flame Chart 읽기

Flame Chart는 컴포넌트 트리를 시각적으로 보여줍니다.

색상의 의미

  • ** 노란색/주황색 **: 렌더링에 상대적으로 오래 걸린 컴포넌트
  • ** 초록색 **: 빠르게 렌더링된 컴포넌트
  • ** 회색 **: 이번 커밋에서 렌더링되지 않은 컴포넌트 (스킵됨)

읽는 방법

PLAINTEXT
App (2.3ms)
├── Header (0.1ms) — 회색 (스킵됨)
├── SearchBar (0.5ms) — 초록
├── ProductList (15.2ms) — 주황 ← 여기가 병목!
│   ├── ProductCard (0.3ms) × 100개
│   └── ...
└── Footer (0.1ms) — 회색 (스킵됨)

ProductList가 15.2ms로 가장 오래 걸렸습니다. 100개의 ProductCard가 모두 리렌더링되고 있다면, 여기를 최적화해야 합니다.

"Why did this render?" 활용

Profiler 설정에서 "Record why each component rendered while profiling" 을 활성화합니다.

리렌더링 원인 유형

  • Props changed: props가 변경됨
  • State changed: 컴포넌트의 state가 변경됨
  • Hooks changed: Hook의 반환값이 변경됨
  • Parent rendered: 부모 컴포넌트가 리렌더링됨 (가장 흔한 불필요 리렌더)

불필요한 리렌더링 패턴

JSX
// 문제: 매 렌더링마다 새 객체/함수를 생성
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>카운트: {count}</button>
      {/* style 객체가 매번 새로 생성됨 */}
      <Child style={{ color: 'red' }} />
      {/* 함수가 매번 새로 생성됨 */}
      <Child onClick={() => console.log('click')} />
    </div>
  );
}

Profiler에서 Child가 "Props changed"로 매번 리렌더링되는 것을 확인할 수 있습니다.

해결

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

  // useMemo로 객체 참조 안정화
  const childStyle = useMemo(() => ({ color: 'red' }), []);

  // useCallback으로 함수 참조 안정화
  const handleClick = useCallback(() => {
    console.log('click');
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>카운트: {count}</button>
      <Child style={childStyle} />
      <Child onClick={handleClick} />
    </div>
  );
}

// React.memo로 props가 같으면 리렌더 스킵
const Child = React.memo(function Child({ style, onClick }) {
  return <div style={style} onClick={onClick}>자식</div>;
});

Ranked Chart

Ranked Chart는 컴포넌트를 렌더링 시간 순으로 정렬합니다.

가장 위에 있는 컴포넌트가 가장 오래 걸린 것입니다. 최적화 우선순위를 정할 때 유용합니다.

PLAINTEXT
ProductList        15.2ms  ████████████████
FilterPanel         3.1ms  ███
SearchResults       2.4ms  ██
ProductCard #47     0.4ms  ▌
ProductCard #12     0.3ms  ▌
...

Highlight Updates 기능

Components 탭에서 "Highlight updates when components render" 를 활성화하면, 리렌더링되는 컴포넌트 주위에 색상 테두리가 표시됩니다.

  • **파란색 **: 빈번하지 않은 업데이트
  • ** 초록색 **: 보통 빈도
  • ** 노란색/빨간색 **: 매우 빈번한 업데이트

입력 필드에 타이핑하면서 전체 페이지가 노란색으로 깜빡이면 문제가 있는 것입니다.

Profiler API — 코드에서 측정

DevTools 외에 코드에서 직접 성능을 측정할 수도 있습니다.

JSX
import { Profiler } from 'react';

function onRenderCallback(
  id,              // Profiler 트리의 id
  phase,           // "mount" 또는 "update"
  actualDuration,  // 이번 렌더링에 소요된 시간
  baseDuration,    // 메모이제이션 없이 전체 서브트리를 렌더링하는 데 걸리는 시간
  startTime,       // 렌더링 시작 시간
  commitTime       // 커밋 시간
) {
  // 성능 데이터 수집
  if (actualDuration > 16) {
    // 60fps 기준 한 프레임(16ms)을 초과하면 경고
    console.warn(`Slow render: ${id} took ${actualDuration.toFixed(2)}ms`);
  }
}

function App() {
  return (
    <Profiler id="ProductList" onRender={onRenderCallback}>
      <ProductList />
    </Profiler>
  );
}

프로덕션에서 성능 모니터링

JSX
function onRenderCallback(id, phase, actualDuration) {
  if (process.env.NODE_ENV === 'production') {
    // 성능 메트릭 수집
    analytics.track('component_render', {
      component: id,
      phase,
      duration: actualDuration,
    });
  }
}

주의: 프로덕션 빌드에서 Profiler를 사용하려면 react-dom/profiling 빌드가 필요합니다.

실전 분석 워크플로

1. 문제 식별

"이 페이지가 느리다"는 막연한 감각에서 시작합니다.

2. Profiler로 녹화

느린 동작(스크롤, 입력, 페이지 전환)을 녹화합니다.

3. Ranked Chart로 병목 찾기

가장 오래 걸리는 컴포넌트를 확인합니다.

4. "Why did this render?"로 원인 파악

불필요한 리렌더링인지, 필요한 리렌더링이 느린 것인지 구분합니다.

5. 최적화 적용

  • ** 불필요한 리렌더 **: React.memo, useMemo, useCallback
  • ** 느린 렌더링 **: 컴포넌트 분리, 가상화, 지연 로딩

6. 다시 측정

최적화 후 다시 Profiler로 측정하여 효과를 확인합니다.

흔한 성능 문제 패턴

Context 값 변경 → 전체 리렌더

JSX
// 문제: context value가 매 렌더마다 새 객체
function Provider({ children }) {
  const [user, setUser] = useState(null);
  // { user, setUser }가 매번 새 객체
  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
}

// 해결: useMemo로 안정화
function Provider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

리스트 전체 리렌더

JSX
// 문제: 한 항목 변경 → 전체 리스트 리렌더
function TodoList({ todos, onToggle }) {
  return todos.map((todo) => (
    <TodoItem
      key={todo.id}
      todo={todo}
      onToggle={() => onToggle(todo.id)} // 매번 새 함수
    />
  ));
}

// 해결: 항목에 id를 전달하고 내부에서 처리
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.text}
    </li>
  );
});

정리

React DevTools Profiler는 성능 최적화의 출발점입니다.

  • Flame Chart 로 컴포넌트별 렌더링 시간을 시각적으로 파악합니다
  • Ranked Chart 로 가장 비용이 큰 컴포넌트를 빠르게 찾습니다
  • "Why did this render?" 로 리렌더링 원인을 정확히 파악합니다
  • Highlight Updates 로 불필요한 리렌더링을 실시간으로 감지합니다
  • Profiler API 로 프로덕션에서도 성능을 모니터링할 수 있습니다

주의할 점

Profiler 측정 없이 React.memo를 무조건 적용하는 실수

측정 없는 최적화는 추측일 뿐입니다. 실제로 성능 병목이 아닌 컴포넌트에 React.memo를 적용하면, 의존성 비교 비용만 추가되고 체감 성능 개선은 없습니다.

개발 모드와 프로덕션 모드의 성능 차이

React DevTools Profiler는 개발 모드에서 동작하는데, 개발 모드는 StrictMode 이중 렌더링, 추가 검사 등으로 프로덕션보다 느립니다. 정확한 성능 측정은 프로덕션 빌드에서 Profiler API를 사용해야 합니다.

Profiler로 실제 병목이 어디인지 확인한 후, 해당 지점만 최적화하는 것이 올바른 접근입니다.

댓글 로딩 중...