2025. 4. 1. 01:12ㆍFrameworks/React
React 애플리케이션 성능 최적화
React 애플리케이션이 복잡해질수록 성능 문제가 발생할 수 있습니다. 이 문서에서는 React 애플리케이션의 성능을 최적화하는 다양한 방법과 기술을 알아보겠습니다.
1. React 렌더링 이해하기
React의 성능을 최적화하기 전에 React의 렌더링 과정을 이해하는 것이 중요합니다.
React의 렌더링 과정
- 상태 변경: 상태(state)나 속성(props)이 변경되면 React는 해당 컴포넌트를 다시 렌더링합니다.
- 가상 DOM 생성: React는 새로운 가상 DOM 트리를 생성합니다.
- 비교(Reconciliation): 이전 가상 DOM과 새로운 가상 DOM을 비교합니다.
- 실제 DOM 업데이트: 변경된 부분만 실제 DOM에 적용합니다.
불필요한 렌더링 문제
React에서 성능 문제의 주요 원인 중 하나는 불필요한 리렌더링입니다. 다음과 같은 경우에 발생할 수 있습니다:
- 부모 컴포넌트가 렌더링될 때 자식 컴포넌트도 자동으로 렌더링
- 객체나 배열과 같은 참조 타입의 새 인스턴스가 매번 생성되는 경우
- 불필요하게 자주 상태가 업데이트되는 경우
2. 성능 측정 도구
최적화하기 전에 먼저 성능을 측정하는 것이 중요합니다.
React DevTools Profiler
React DevTools의 Profiler 탭을 사용하면 컴포넌트의 렌더링 성능을 분석할 수 있습니다.
- Chrome 확장 프로그램으로 React DevTools 설치
- 개발자 도구 > Components 탭에서 Profiler 선택
- Record 버튼 클릭 후 애플리케이션 사용
- 결과 분석하여 오래 걸리는 컴포넌트 확인
Performance 탭
브라우저의 개발자 도구에 있는 Performance 탭을 사용하면 더 자세한 성능 정보를 얻을 수 있습니다.
- 개발자 도구 > Performance 탭 선택
- Record 버튼 클릭 후 애플리케이션 사용
- 타임라인에서 렌더링 시간, 자바스크립트 실행 시간 등 확인
Lighthouse
Google의 Lighthouse 도구를 사용하여 웹 애플리케이션의 전반적인 성능을 측정할 수 있습니다.
- 개발자 도구 > Lighthouse 탭 선택
- 분석할 항목 선택하고 'Generate report' 클릭
- 성능 점수와 개선 사항 확인
3. 메모이제이션을 통한 최적화
메모이제이션은 이전에 계산된 결과를 저장하여 동일한 입력에 대해 계산을 반복하지 않는 기법입니다.
React.memo
React.memo는 고차 컴포넌트(HOC)로, 컴포넌트의 props가 변경되지 않으면 리렌더링을 방지합니다.
import React from 'react';
// 일반 컴포넌트
function MovieCard({ title, rating, year }) {
console.log(`Rendering: ${title}`);
return (
<div className="movie-card">
<h3>{title}</h3>
<p>Rating: {rating}/10</p>
<p>Year: {year}</p>
</div>
);
}
// 메모이제이션된 컴포넌트
const MemoizedMovieCard = React.memo(MovieCard);
export default MemoizedMovieCard;
React.memo는 props를 얕게 비교합니다. 객체나 배열 같은 참조 타입은 내용이 같더라도 참조가 다르면 다시 렌더링됩니다. 이런 경우에는 두 번째 인자로 커스텀 비교 함수를 전달할 수 있습니다:
const MemoizedMovieCard = React.memo(
MovieCard,
(prevProps, nextProps) => {
// true를 반환하면 리렌더링하지 않음
// 여기서는 객체의 내용을 비교
return prevProps.title === nextProps.title &&
prevProps.rating === nextProps.rating &&
prevProps.year === nextProps.year;
}
);
useMemo
useMemo 훅은 계산 비용이 큰 값을 메모이제이션합니다. 의존성 배열의 값이 변경될 때만 다시 계산합니다.
import React, { useMemo, useState } from 'react';
function ExpensiveCalculation({ numbers }) {
const [count, setCount] = useState(0);
// 비용이 큰 계산 (의존성 배열의 값이 변경될 때만 다시 계산)
const sum = useMemo(() => {
console.log('Computing sum...');
return numbers.reduce((acc, val) => acc + val, 0);
}, [numbers]);
return (
<div>
<p>Sum: {sum}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
</div>
);
}
위 예제에서 count 상태가 변경되어도 numbers 배열이 변경되지 않으면 sum을 다시 계산하지 않습니다.
useCallback
useCallback 훅은 함수를 메모이제이션합니다. 의존성 배열의 값이 변경될 때만 새 함수를 생성합니다.
import React, { useState, useCallback } from 'react';
import ExpensiveChildComponent from './ExpensiveChildComponent';
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 메모이제이션된 함수 (text가 변경될 때만 새 함수 생성)
const handleProcess = useCallback(() => {
console.log(`Processing: ${text}`);
// text를 처리하는 로직...
}, [text]);
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Enter text"
/>
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
{/* 메모이제이션된 함수를 자식 컴포넌트에 전달 */}
<ExpensiveChildComponent onProcess={handleProcess} />
</div>
);
}
위 예제에서 count 상태가 변경되어도 text가 변경되지 않으면 handleProcess 함수는 새로 생성되지 않습니다. 이것은 ExpensiveChildComponent가 불필요하게 리렌더링되는 것을 방지합니다.
4. 상태 관리 최적화
상태 구조화
상태를 적절히 구조화하면 불필요한 리렌더링을 방지할 수 있습니다.
세분화된 상태 사용
큰 상태 객체 대신 여러 작은 상태로 나누면 필요한 부분만 업데이트할 수 있습니다.
// 좋지 않은 방식: 큰 객체를 하나의 상태로 관리
const [user, setUser] = useState({
name: 'John',
email: 'john@example.com',
preferences: {
theme: 'dark',
notifications: true
}
});
// 더 나은 방식: 관련 상태를 분리
const [name, setName] = useState('John');
const [email, setEmail] = useState('john@example.com');
const [preferences, setPreferences] = useState({
theme: 'dark',
notifications: true
});
컴포넌트 분할
상태가 변경되는 부분을 작은 컴포넌트로 분리하여 리렌더링 범위를 줄일 수 있습니다.
function ParentComponent() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: 'John' });
return (
<div>
<CounterComponent count={count} setCount={setCount} />
<UserComponent user={user} setUser={setUser} />
</div>
);
}
// count가 변경되어도 UserComponent는 리렌더링되지 않음
const CounterComponent = React.memo(({ count, setCount }) => {
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
});
// user가 변경되어도 CounterComponent는 리렌더링되지 않음
const UserComponent = React.memo(({ user, setUser }) => {
return (
<div>
<p>User: {user.name}</p>
<button onClick={() => setUser({ name: 'Jane' })}>Change User</button>
</div>
);
});
Context API 최적화
Context API를 사용할 때도 성능을 고려해야 합니다.
컨텍스트 분리
서로 관련 없는 상태는 별도의 컨텍스트로 분리하세요.
// 좋지 않은 방식: 모든 상태를 하나의 컨텍스트에 넣기
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme, cart, setCart }}>
{children}
</AppContext.Provider>
);
}
// 더 나은 방식: 관련 상태를 분리된 컨텍스트로 관리
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();
function AppProvider({ children }) {
return (
<UserProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</UserProvider>
);
}
컨텍스트 값 메모이제이션
컨텍스트 값을 메모이제이션하여 불필요한 리렌더링을 방지할 수 있습니다.
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// 컨텍스트 값 메모이제이션
const themeContextValue = useMemo(() => ({
theme,
setTheme
}), [theme]);
return (
<ThemeContext.Provider value={themeContextValue}>
{children}
</ThemeContext.Provider>
);
}
5. 리스트 렌더링 최적화
리스트는 많은 항목을 렌더링하기 때문에 최적화가 중요합니다.
고유 키 사용
리스트 항목에는 항상 안정적이고 고유한 key 속성을 사용해야 합니다.
// 좋지 않은 방식: 인덱스를 키로 사용
{items.map((item, index) => (
<ListItem key={index} item={item} />
))}
// 더 나은 방식: 고유 ID를 키로 사용
{items.map(item => (
<ListItem key={item.id} item={item} />
))}
인덱스를 키로 사용하면 항목의 순서가 변경될 때 불필요한 리렌더링이 발생하거나 상태가 의도치 않게 유지될 수 있습니다.
가상화(Virtualization)
많은 수의 항목을 렌더링할 때는 화면에 보이는 항목만 렌더링하는 가상화 기법을 사용할 수 있습니다.
react-window 또는 react-virtualized 같은 라이브러리를 사용할 수 있습니다.
import React from 'react';
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
// 각 항목을 렌더링하는 함수
const Row = ({ index, style }) => (
<div style={style}>
<div>{items[index].name}</div>
<div>{items[index].description}</div>
</div>
);
return (
<FixedSizeList
height={500} // 리스트의 높이
width="100%" // 리스트의 너비
itemCount={items.length} // 총 항목 수
itemSize={50} // 각 항목의 높이
>
{Row}
</FixedSizeList>
);
}
6. 코드 분할 및 지연 로딩
대규모 애플리케이션에서는 코드 분할과 지연 로딩을 통해 초기 로드 시간을 줄일 수 있습니다.
React.lazy와 Suspense
React.lazy와 Suspense를 사용하면 컴포넌트를 동적으로 로드할 수 있습니다.
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// 동적으로 컴포넌트 로드
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
);
}
조건부 임포트
특정 조건에서만 사용되는 큰 라이브러리는 조건부로 임포트할 수 있습니다.
import React, { useState } from 'react';
function ChartComponent() {
const [showChart, setShowChart] = useState(false);
const [Chart, setChart] = useState(null);
const loadChart = async () => {
if (!Chart) {
// 사용자가 차트를 보고 싶을 때만 차트 라이브러리 로드
const module = await import('react-chartjs-2');
setChart(() => module.Line);
}
setShowChart(true);
};
return (
<div>
<button onClick={loadChart}>
{showChart ? 'Hide Chart' : 'Show Chart'}
</button>
{showChart && Chart && (
<Chart
data={chartData}
options={chartOptions}
/>
)}
</div>
);
}
7. 서버 사이드 렌더링(SSR) 및 정적 사이트 생성(SSG)
클라이언트 사이드 렌더링만으로는 초기 로드 성능에 한계가 있습니다. 서버 사이드 렌더링이나 정적 사이트 생성을 통해 초기 로드 성능을 개선할 수 있습니다.
Next.js를 활용한 SSR/SSG
Next.js는 React 기반 프레임워크로, SSR과 SSG를 쉽게 구현할 수 있습니다.
서버 사이드 렌더링 (SSR)
// pages/products/[id].js
export async function getServerSideProps({ params }) {
// 서버에서 데이터 가져오기
const res = await fetch(`https://api.example.com/products/${params.id}`);
const product = await res.json();
return {
props: {
product
}
};
}
function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</div>
);
}
export default ProductPage;
정적 사이트 생성 (SSG)
// pages/posts/[slug].js
export async function getStaticPaths() {
// 가능한 모든 경로를 미리 결정
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
const paths = posts.map(post => ({
params: { slug: post.slug }
}));
return { paths, fallback: false };
}
export async function getStaticProps({ params }) {
// 빌드 시 데이터 가져오기
const res = await fetch(`https://api.example.com/posts/${params.slug}`);
const post = await res.json();
return {
props: {
post
}
};
}
function PostPage({ post }) {
return (
<div>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
);
}
export default PostPage;
8. Web Worker 활용
무거운 계산 작업은 메인 스레드 대신 Web Worker를 사용하여 UI의 응답성을 유지할 수 있습니다.
// worker.js
self.addEventListener('message', (e) => {
if (e.data.type === 'calculate') {
// 시간이 오래 걸리는 계산 수행
const result = performHeavyCalculation(e.data.numbers);
self.postMessage({ type: 'result', result });
}
});
function performHeavyCalculation(numbers) {
// 무거운 계산 작업
return numbers.reduce((sum, num) => {
// 인위적으로 부하 생성
let result = sum;
for (let i = 0; i < 1000000; i++) {
result += Math.sqrt(num);
}
return result;
}, 0);
}
// React 컴포넌트
import React, { useState, useEffect } from 'react';
function HeavyCalculation() {
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
const [result, setResult] = useState(null);
const [isCalculating, setIsCalculating] = useState(false);
const [worker, setWorker] = useState(null);
useEffect(() => {
// Web Worker 생성
const newWorker = new Worker('/worker.js');
newWorker.addEventListener('message', (e) => {
if (e.data.type === 'result') {
setResult(e.data.result);
setIsCalculating(false);
}
});
setWorker(newWorker);
// 컴포넌트 언마운트 시 Worker 종료
return () => {
newWorker.terminate();
};
}, []);
const handleCalculate = () => {
if (worker) {
setIsCalculating(true);
worker.postMessage({
type: 'calculate',
numbers
});
}
};
return (
<div>
<div>
<input
type="text"
value={numbers.join(', ')}
onChange={(e) => setNumbers(e.target.value.split(',').map(n => parseFloat(n.trim())))}
/>
</div>
<button onClick={handleCalculate} disabled={isCalculating}>
{isCalculating ? '계산 중...' : '계산 시작'}
</button>
{result !== null && (
<div>결과: {result}</div>
)}
</div>
);
}
9. 이미지 및 애셋 최적화
이미지 최적화
큰 이미지 파일은 로딩 시간을 늘리고 성능에 영향을 줍니다.
- 적절한 크기의 이미지 사용
- WebP와 같은 최신 이미지 형식 사용
- 이미지 압축
- 지연 로딩 적용
Next.js의 Image 컴포넌트나 라이브러리를 사용할 수 있습니다:
// Next.js Image 컴포넌트
import Image from 'next/image';
function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<Image
src={product.image}
alt={product.name}
width={500}
height={300}
placeholder="blur" // 로딩 중 블러 효과
blurDataURL={product.thumbnailDataUrl}
/>
</div>
);
}
// React에서 LazyLoad 라이브러리 사용
import LazyLoad from 'react-lazyload';
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<LazyLoad
key={product.id}
height={300}
offset={100}
placeholder={<div className="loading-placeholder">Loading...</div>}
>
<img
src={product.image}
alt={product.name}
className="product-image"
/>
</LazyLoad>
))}
</div>
);
}
폰트 최적화
웹 폰트는 로딩 시간에 영향을 줄 수 있습니다.
- 필요한 글꼴 스타일과 가중치만 로드
- 폰트 표시 전략 설정 (font-display 속성)
- preload나 preconnect를 사용한 폰트 파일 최적화
@font-face {
font-family: 'CustomFont';
src: url('/fonts/CustomFont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* 폰트 로딩 전에 시스템 폰트 사용 */
}
10. 불변성 유지하기
React에서 성능을 최적화하려면 상태 업데이트 시 불변성을 유지하는 것이 중요합니다.
불변 업데이트 패턴
// 배열에 항목 추가
setItems([...items, newItem]);
// 배열에서 항목 제거
setItems(items.filter(item => item.id !== idToRemove));
// 배열에서 항목 업데이트
setItems(items.map(item =>
item.id === idToUpdate ? { ...item, ...updatedValues } : item
));
// 객체 속성 업데이트
setUser({
...user,
name: 'New Name'
});
// 중첩 객체 업데이트
setUser({
...user,
address: {
...user.address,
city: 'New City'
}
});
Immer 사용하기
복잡한 중첩 상태 업데이트는 Immer 라이브러리를 사용하면 더 쉽게 처리할 수 있습니다.
import produce from 'immer';
// Immer 없이 복잡한 중첩 상태 업데이트
setUser({
...user,
address: {
...user.address,
contacts: user.address.contacts.map(contact =>
contact.id === contactId
? { ...contact, email: newEmail }
: contact
)
}
});
// Immer를 사용한 더 간단한 업데이트
setUser(produce(user, draft => {
const contact = draft.address.contacts.find(c => c.id === contactId);
if (contact) {
contact.email = newEmail;
}
}));
11. 실제 성능 최적화 사례
예시 1: 무거운 리스트 최적화
다음은 수천 개의 항목을 효율적으로 렌더링하는 최적화된 리스트 예제입니다:
import React, { useState, useMemo, useCallback } from 'react';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
function OptimizedList({ items, onItemSelect }) {
const [selectedId, setSelectedId] = useState(null);
// 필터 및 정렬 로직 메모이제이션
const processedItems = useMemo(() => {
console.log('Processing items...');
return items
.filter(item => item.active)
.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// 항목 선택 핸들러 메모이제이션
const handleSelectItem = useCallback((item) => {
setSelectedId(item.id);
onItemSelect(item);
}, [onItemSelect]);
// 각 행 렌더링 함수 메모이제이션
const Row = useCallback(({ index, style }) => {
const item = processedItems[index];
const isSelected = selectedId === item.id;
return (
<div
style={{
...style,
backgroundColor: isSelected ? '#e0e0e0' : 'white',
display: 'flex',
alignItems: 'center',
padding: '0 10px'
}}
onClick={() => handleSelectItem(item)}
>
<img
src={item.avatar}
alt={item.name}
style={{ width: 40, height: 40, borderRadius: '50%', marginRight: 10 }}
/>
<div>
<div style={{ fontWeight: 'bold' }}>{item.name}</div>
<div>{item.email}</div>
</div>
</div>
);
}, [processedItems, selectedId, handleSelectItem]);
return (
<div style={{ height: '500px', width: '100%' }}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
width={width}
itemCount={processedItems.length}
itemSize={60}
>
{Row}
</FixedSizeList>
)}
</AutoSizer>
</div>
);
}
예시 2: 데이터 그리드 최적화
대량의 데이터를 표시하는 그리드 컴포넌트의 성능 최적화:
import React, { useState, useCallback, useMemo } from 'react';
import { FixedSizeGrid } from 'react-window';
function DataGrid({ data, columns, onCellClick }) {
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
// 정렬 로직 메모이제이션
const sortedData = useMemo(() => {
if (!sortConfig.key) return data;
return [...data].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}, [data, sortConfig]);
// 열 헤더 클릭 핸들러
const handleHeaderClick = useCallback((key) => {
setSortConfig(prevConfig => ({
key,
direction: prevConfig.key === key && prevConfig.direction === 'asc' ? 'desc' : 'asc'
}));
}, []);
// 셀 클릭 핸들러
const handleCellClick = useCallback((rowIndex, columnIndex) => {
const row = sortedData[rowIndex];
const column = columns[columnIndex];
onCellClick({ row, column });
}, [sortedData, columns, onCellClick]);
// 셀 렌더링 함수
const Cell = useCallback(({ columnIndex, rowIndex, style }) => {
const row = sortedData[rowIndex];
const column = columns[columnIndex];
const value = row[column.key];
return (
<div
style={{
...style,
padding: '8px',
borderBottom: '1px solid #eee',
display: 'flex',
alignItems: 'center',
justifyContent: column.align || 'left',
cursor: 'pointer'
}}
onClick={() => handleCellClick(rowIndex, columnIndex)}
>
{column.render ? column.render(value, row) : value}
</div>
);
}, [sortedData, columns, handleCellClick]);
// 헤더 컴포넌트
const Header = useMemo(() => (
<div style={{ display: 'flex', fontWeight: 'bold', borderBottom: '2px solid #ccc' }}>
{columns.map((column, index) => (
<div
key={column.key}
style={{
width: column.width || 150,
padding: '8px',
cursor: 'pointer',
textAlign: column.align || 'left'
}}
onClick={() => handleHeaderClick(column.key)}
>
{column.title}
{sortConfig.key === column.key && (
<span>{sortConfig.direction === 'asc' ? ' ▲' : ' ▼'}</span>
)}
</div>
))}
</div>
), [columns, sortConfig, handleHeaderClick]);
return (
<div>
{Header}
<FixedSizeGrid
columnCount={columns.length}
columnWidth={index => columns[index].width || 150}
height={500}
rowCount={sortedData.length}
rowHeight={40}
width={900}
>
{Cell}
</FixedSizeGrid>
</div>
);
}
12. 성능 관련 Anti-patterns
성능을 저하시키는 흔한 패턴과 그 대안입니다:
1. 컴포넌트 내 불필요한 함수 생성
렌더링마다 새로운 함수를 생성하면 불필요한 리렌더링이 발생할 수 있습니다.
// 안 좋은 방식
function BadComponent({ item, onUpdate }) {
// 매 렌더링마다 새로운 함수 생성
const handleClick = () => {
onUpdate(item.id);
};
return (
<button onClick={handleClick}>
Update {item.name}
</button>
);
}
// 좋은 방식
function GoodComponent({ item, onUpdate }) {
// useCallback으로 함수 메모이제이션
const handleClick = useCallback(() => {
onUpdate(item.id);
}, [item.id, onUpdate]);
return (
<button onClick={handleClick}>
Update {item.name}
</button>
);
}
2. 인라인 객체 리터럴
인라인 객체나 배열을 props로 전달하면 매 렌더링마다 새 참조가 생성됩니다.
// 안 좋은 방식
function BadParent() {
return (
<Child
// 매 렌더링마다 새 객체 생성
style={{ margin: 20, padding: 10 }}
// 매 렌더링마다 새 배열 생성
items={[1, 2, 3]}
/>
);
}
// 좋은 방식
function GoodParent() {
// 객체를 메모이제이션
const style = useMemo(() => ({
margin: 20,
padding: 10
}), []);
// 배열을 메모이제이션
const items = useMemo(() => [1, 2, 3], []);
return (
<Child
style={style}
items={items}
/>
);
}
3. 과도한 Prop Drilling
Props를 여러 계층의 컴포넌트를 통해 전달하면 중간 컴포넌트가 불필요하게 리렌더링될 수 있습니다.
// 안 좋은 방식: 깊은 Prop Drilling
function App() {
const [user, setUser] = useState({ name: 'John' });
return <Parent user={user} />;
}
function Parent({ user }) {
return <Child user={user} />;
}
function Child({ user }) {
return <Grandchild user={user} />;
}
function Grandchild({ user }) {
return <div>{user.name}</div>;
}
// 좋은 방식: Context 사용
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'John' });
const userContextValue = useMemo(() => ({
user
}), [user]);
return (
<UserContext.Provider value={userContextValue}>
<Parent />
</UserContext.Provider>
);
}
function Parent() {
return <Child />;
}
function Child() {
return <Grandchild />;
}
function Grandchild() {
const { user } = useContext(UserContext);
return <div>{user.name}</div>;
}
결론
React 애플리케이션 성능 최적화는 단일 기법보다는 여러 접근 방식의 조합으로 이루어집니다. 애플리케이션의 특성과 요구사항에 맞는 최적화 전략을 선택하는 것이 중요합니다.
성능 최적화를 위한 핵심 원칙:
- 측정 먼저: 성능 문제를 추측하지 말고 측정 도구로 확인하세요.
- 점진적 최적화: 한 번에 모든 것을 최적화하려 하지 말고, 가장 문제가 되는 부분부터 점진적으로 개선하세요.
- 선택적 렌더링: 필요한 컴포넌트만 리렌더링되도록 구조화하세요.
- 가상화 및 지연 로딩: 대량의 데이터나 컴포넌트를 다룰 때 모든 것을 한 번에 처리하지 마세요.
- 불변성 유지: 불변 업데이트 패턴을 사용하여 변경 감지를 최적화하세요.
- 메모이제이션 활용: React.memo, useMemo, useCallback을 적절히 사용하여 계산과 렌더링을 최적화하세요.
React 애플리케이션의 성능을 지속적으로 모니터링하고 개선하는 것은 좋은 사용자 경험을 제공하는 데 필수적입니다.
'Frameworks > React' 카테고리의 다른 글
테스트 전략 (Jest, RTL, Cypress) (0) | 2025.04.02 |
---|---|
React + TypeScript 도입과 패턴 (0) | 2025.04.02 |
React 폼 관리와 유효성 검사 (0) | 2025.04.01 |
React 컴포넌트 스타일링 방법 (0) | 2025.04.01 |
에러 처리와 로딩 상태 관리 (0) | 2025.04.01 |