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 확장 프로그램 설치
사용 방법
- 개발자 도구 열기 (F12)
- "Components" 또는 "Profiler" 탭 선택
- Profiler 탭에서 녹화 버튼 클릭
- 애플리케이션 사용
- 녹화 중지
- 결과 분석
무엇을 확인하나요?
- 렌더링이 자주 발생하는 컴포넌트
- 렌더링에 시간이 많이 걸리는 컴포넌트
- 불필요한 렌더링이 발생하는 지점
Lighthouse
Lighthouse는 웹페이지의 전반적인 성능을 측정하는 도구입니다.
사용 방법
- Chrome 개발자 도구 열기
- Lighthouse 탭 선택
- 분석하고 싶은 항목 선택 (성능, 접근성, SEO 등)
- 분석 시작 버튼 클릭
주요 측정 지표
- First Contentful Paint (FCP): 첫 콘텐츠가 표시되는 시간
- Largest Contentful Paint (LCP): 가장 큰 콘텐츠가 표시되는 시간
- Time to Interactive (TTI): 사용자와 상호작용 가능한 시간
- Total Blocking Time (TBT): 메인 스레드가 막힌 총 시간
- Cumulative Layout Shift (CLS): 페이지 로드 중 레이아웃 변화량
Chrome Performance 탭
복잡한 성능 문제를 더 자세히 분석할 때 사용합니다.
사용 방법
- Chrome 개발자 도구 열기
- Performance 탭 선택
- 녹화 버튼 클릭
- 애플리케이션 사용
- 녹화 중지
- 결과 분석 (프레임 속도, 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로 렌더링과 계산 최적화하기
- 코드 분할과 지연 로딩으로 초기 로딩 시간 줄이기
- 가상화 기법으로 대량의 데이터 처리하기
- 개발 및 프로덕션 환경에서 지속적으로 성능 측정하기