Frameworks/React

React 성능 분석 실전

newclass 2025. 4. 2. 16:38

React 성능 분석 실전

React 성능이 왜 중요한가요?

성능이 좋은 앱은 사용자 경험을 향상시키고, 더 많은 사용자를 유지할 수 있습니다. 느린 앱은 사용자를 떠나게 만듭니다.

성능 문제의 주요 증상

  • 화면 전환이 느림
  • 스크롤이 부드럽지 않음
  • 버튼 클릭 후 반응이 지연됨
  • 입력 시 타이핑 지연
  • 메모리 사용량이 계속 증가함

성능 문제 발견하기

React Developer Tools Profiler

React Developer Tools의 Profiler는 컴포넌트의 렌더링 시간을 측정합니다.

설치 방법

  • Chrome/Edge/Firefox에서 React Developer Tools 확장 프로그램 설치

사용 방법

  1. 개발자 도구 열기 (F12)
  2. "Components" 또는 "Profiler" 탭 선택
  3. Profiler 탭에서 녹화 버튼 클릭
  4. 애플리케이션 사용
  5. 녹화 중지
  6. 결과 분석

무엇을 확인하나요?

  • 렌더링이 자주 발생하는 컴포넌트
  • 렌더링에 시간이 많이 걸리는 컴포넌트
  • 불필요한 렌더링이 발생하는 지점

Lighthouse

Lighthouse는 웹페이지의 전반적인 성능을 측정하는 도구입니다.

사용 방법

  1. Chrome 개발자 도구 열기
  2. Lighthouse 탭 선택
  3. 분석하고 싶은 항목 선택 (성능, 접근성, SEO 등)
  4. 분석 시작 버튼 클릭

주요 측정 지표

  • First Contentful Paint (FCP): 첫 콘텐츠가 표시되는 시간
  • Largest Contentful Paint (LCP): 가장 큰 콘텐츠가 표시되는 시간
  • Time to Interactive (TTI): 사용자와 상호작용 가능한 시간
  • Total Blocking Time (TBT): 메인 스레드가 막힌 총 시간
  • Cumulative Layout Shift (CLS): 페이지 로드 중 레이아웃 변화량

Chrome Performance 탭

복잡한 성능 문제를 더 자세히 분석할 때 사용합니다.

사용 방법

  1. Chrome 개발자 도구 열기
  2. Performance 탭 선택
  3. 녹화 버튼 클릭
  4. 애플리케이션 사용
  5. 녹화 중지
  6. 결과 분석 (프레임 속도, CPU 사용량, 이벤트 등)

성능 최적화 기법

1. 불필요한 렌더링 방지하기

React 컴포넌트는 다음과 같은 경우에 다시 렌더링됩니다:

  • 자신의 state가 변경될 때
  • 부모 컴포넌트가 다시 렌더링될 때
  • context가 변경될 때

React.memo 사용하기

// React.memo로 컴포넌트 메모이제이션
import { memo } from 'react';

// props가 변경되지 않으면 다시 렌더링하지 않음
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
  // 비용이 많이 드는 렌더링 로직
  return <div>{/* ... */}</div>;
});

언제 React.memo를 사용해야 할까요?

  • 자주 렌더링되는 컴포넌트
  • 렌더링 비용이 높은 컴포넌트
  • props가 자주 변경되지 않는 컴포넌트

2. 콜백 함수 최적화하기

부모 컴포넌트가 렌더링될 때마다 새로운 함수가 생성되면, React.memo를 사용해도 자식 컴포넌트가 다시 렌더링됩니다.

useCallback 사용하기

import { useCallback } from 'react';

function ParentComponent() {
  // 렌더링마다 새로운 함수 생성 방지
  const handleClick = useCallback(() => {
    console.log('버튼 클릭됨');
  }, []); // 의존성 배열이 비어있으므로 함수는 한 번만 생성됨
  
  return <Button onClick={handleClick} />;
}

의존성 배열 올바르게 사용하기

function SearchComponent({ query }) {
  const handleSearch = useCallback(() => {
    // query를 사용하는 검색 로직
    fetchResults(query);
  }, [query]); // query가 변경될 때만 함수 재생성
  
  return <Button onClick={handleSearch} label="검색" />;
}

3. 계산 결과 캐싱하기

복잡한 계산이 렌더링마다 반복되면 성능 저하의 원인이 될 수 있습니다.

useMemo 사용하기

import { useMemo } from 'react';

function DataList({ items, filter }) {
  // filter가 변경될 때만 다시 계산
  const filteredItems = useMemo(() => {
    console.log('아이템 필터링...');
    return items.filter(item => item.name.includes(filter));
  }, [items, filter]);
  
  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

언제 useMemo를 사용해야 할까요?

  • 계산 비용이 높은 작업
  • 렌더링 사이에 결과가 변경되지 않는 작업
  • 참조 동등성이 중요한 값 (하위 컴포넌트의 props로 전달되는 객체나 배열)

4. 상태 관리 최적화하기

상태 구조가 복잡하거나 불필요하게 전체 상태가 변경되면 성능 문제가 발생할 수 있습니다.

상태 분할하기

// 안 좋은 예: 하나의 큰 상태 객체
const [state, setState] = useState({
  name: '',
  email: '',
  address: '',
  isLoading: false,
  error: null,
  data: []
});

// 좋은 예: 관련된 상태끼리 분리
const [userInfo, setUserInfo] = useState({ name: '', email: '', address: '' });
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState([]);

불변성 지키기

// 안 좋은 예: 직접 상태 수정
const handleChange = (index, newValue) => {
  const newItems = items;
  newItems[index].value = newValue; // ❌ 불변성 위반
  setItems(newItems);
};

// 좋은 예: 새 배열 생성
const handleChange = (index, newValue) => {
  const newItems = items.map((item, i) => 
    i === index ? { ...item, value: newValue } : item
  );
  setItems(newItems);
};

5. 코드 분할과 지연 로딩

큰 번들 크기는 초기 로딩 시간을 증가시킵니다.

React.lazy와 Suspense 사용하기

import React, { Suspense, lazy } from 'react';

// 동적으로 컴포넌트 import
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>로딩 중...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

라우트 기반 코드 분할

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./Home'));
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>페이지 로딩 중...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

실전 성능 최적화 팁

1. 가상화 목록 사용하기

많은 항목을 표시할 때는 react-window나 react-virtualized 같은 라이브러리를 사용하여 화면에 보이는 항목만 렌더링합니다.

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const renderRow = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={500}
      width={300}
      itemCount={items.length}
      itemSize={35}
    >
      {renderRow}
    </FixedSizeList>
  );
}

2. 이미지 최적화

  • 적절한 크기와 형식의 이미지 사용
  • 지연 로딩 적용
  • WebP와 같은 현대적 이미지 형식 사용
function Image({ src, alt }) {
  return (
    <img 
      src={src} 
      alt={alt} 
      loading="lazy" // 화면에 들어올 때만 로딩
      width={300} 
      height={200} 
    />
  );
}

3. 서버 상태 최적화

React Query 같은 라이브러리를 사용하여 데이터 가져오기, 캐싱, 재검증을 최적화합니다.

import { useQuery } from 'react-query';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery(
    ['user', userId], 
    () => fetchUser(userId),
    { staleTime: 5 * 60 * 1000 } // 5분 동안 캐시 유지
  );
  
  if (isLoading) return <div>로딩 중...</div>;
  if (error) return <div>오류 발생</div>;
  
  return <div>{data.name}</div>;
}

성능 측정 및 모니터링

개발 중 성능 측정

  • React DevTools Profiler 정기적으로 사용하기
  • 코드 변경 전후 성능 측정하기
  • Lighthouse 점수 확인하기

프로덕션 환경 모니터링

  • web-vitals 라이브러리로 실제 사용자 측정치 수집
  • Google Analytics나 성능 모니터링 서비스에 데이터 전송
import { getCLS, getFID, getLCP } from 'web-vitals';

function sendToAnalytics({ name, delta, id }) {
  // 분석 서비스로 성능 데이터 전송
  console.log(`${name}: ${delta}ms (ID: ${id})`);
}

// Core Web Vitals 측정
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

요약

  • React 성능 문제는 주로 불필요한 렌더링과 무거운 계산에서 발생합니다
  • React.memo, useCallback, useMemo로 렌더링과 계산 최적화하기
  • 코드 분할과 지연 로딩으로 초기 로딩 시간 줄이기
  • 가상화 기법으로 대량의 데이터 처리하기
  • 개발 및 프로덕션 환경에서 지속적으로 성능 측정하기