본문 바로가기
Frontend2025년 3월 15일8분 읽기

React 성능 프로파일링 — DevTools로 병목 찾기

YS
김영삼
조회 536

React 성능 문제의 주요 원인

React 애플리케이션에서 가장 흔한 성능 문제는 불필요한 리렌더링입니다. 부모 컴포넌트가 렌더링되면 모든 자식이 함께 렌더링되는 React의 기본 동작이 큰 컴포넌트 트리에서 성능 저하를 유발합니다.

DevTools Profiler 사용법

React DevTools의 Profiler 탭에서 렌더링 기록을 시작하고, 문제 상황을 재현한 뒤 기록을 중지합니다. Flamegraph에서 각 컴포넌트의 렌더링 시간과 원인을 확인할 수 있습니다.

// 개발 모드에서 리렌더링 원인 추적
// React DevTools Settings > Profiler > Record why each component rendered

// 프로그래밍 방식으로 렌더링 추적
import { Profiler } from 'react';

function onRenderCallback(
  id,           // Profiler 트리의 id
  phase,        // "mount" 또는 "update"
  actualDuration,   // 실제 렌더링 시간
  baseDuration,     // 메모이제이션 없을 때 예상 시간
  startTime,
  commitTime
) {
  console.table({ id, phase, actualDuration, baseDuration });
}

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

불필요한 리렌더링 방지

// 1. React.memo로 props 변경 시에만 리렌더링
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

// 2. useMemo로 비용 큰 계산 캐싱
function Dashboard({ transactions }) {
  const summary = useMemo(() => {
    return transactions.reduce((acc, tx) => {
      acc.total += tx.amount;
      acc.count += 1;
      acc.categories[tx.category] = (acc.categories[tx.category] || 0) + tx.amount;
      return acc;
    }, { total: 0, count: 0, categories: {} });
  }, [transactions]);

  return <SummaryCard data={summary} />;
}

// 3. useCallback으로 함수 참조 안정화
function Parent() {
  const [selected, setSelected] = useState(null);

  const handleSelect = useCallback((id) => {
    setSelected(id);
  }, []);

  return <ExpensiveList items={items} onSelect={handleSelect} />;
}

상태 구조 최적화

// BAD: 하나의 큰 상태 — 어떤 변경이든 전체 리렌더링
const [state, setState] = useState({
  user: null,
  theme: 'light',
  notifications: [],
  sidebarOpen: false,
});

// GOOD: 상태 분리 — 변경된 부분만 리렌더링 유발
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(false);

// BETTER: Context 분리로 구독 범위 최소화
const ThemeContext = createContext();
const UserContext = createContext();
const NotificationContext = createContext();

가상화로 대량 렌더링 최적화

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualizedList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: virtualRow.start,
              height: virtualRow.size,
              width: '100%',
            }}
          >
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

프로파일링 체크리스트

항목도구확인 사항
리렌더링 횟수React DevTools Profiler불필요한 리렌더링 식별
번들 크기webpack-bundle-analyzer큰 의존성 확인
메모리 누수Chrome Memory 탭힙 스냅샷 비교
런타임 성능Chrome Performance 탭Long Task 식별
  • 최적화는 측정 먼저, 추측하지 말 것 — Profiler로 실제 병목을 확인한 후 최적화
  • React.memo는 props가 자주 바뀌는 컴포넌트에 적용하면 오히려 오버헤드가 됩니다
  • 상태를 가능한 사용하는 곳 가까이에 배치하면 리렌더링 범위가 자연스럽게 줄어듭니다

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록