React 요약 9 - 성능 최적화
Updated:
React.memo
import { useState, memo } from 'react';
// 일반 컴포넌트
function RegularComponent({ name, age }) {
console.log('RegularComponent rendered');
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
</div>
);
}
// memo로 최적화된 컴포넌트
const MemoizedComponent = memo(function MemoizedComponent({ name, age }) {
console.log('MemoizedComponent rendered');
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
</div>
);
});
// 커스텀 비교 함수를 사용한 memo
const MemoizedWithCustomCompare = memo(
function MemoizedWithCustomCompare({ user, onUserClick }) {
console.log('MemoizedWithCustomCompare rendered');
return (
<div onClick={() => onUserClick(user.id)}>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
</div>
);
},
(prevProps, nextProps) => {
// 사용자 정의 비교 로직
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.age === nextProps.user.age &&
prevProps.onUserClick === nextProps.onUserClick
);
}
);
// 부모 컴포넌트
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('John');
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setName(name === 'John' ? 'Jane' : 'John')}>
Toggle Name
</button>
<h2>Regular Component (always re-renders)</h2>
<RegularComponent name={name} age={30} />
<h2>Memoized Component (re-renders only when props change)</h2>
<MemoizedComponent name={name} age={30} />
</div>
);
}
코멘트:
React.memo
(또는 함수형 컴포넌트에서memo
)는 고차 컴포넌트(HOC)로, 컴포넌트의 props가 변경되지 않았을 때 리렌더링을 방지합니다. 이는 다음과 같은 경우에 유용합니다:
- 컴포넌트가 같은 props로 자주 렌더링될 때
- 컴포넌트가 렌더링 비용이 많이 들 때
- 컴포넌트가 순수한 함수형일 때 (props에만 의존)
기본적으로 얕은 비교를 수행하지만, 두 번째 인자로 사용자 정의 비교 함수를 제공할 수 있습니다. 모든 컴포넌트를 memo로 감싸는 것은 오히려 성능을 저하시킬 수 있으므로, 실제 성능 이슈가 있는 경우에만 사용하세요.
useMemo와 useCallback
import { useState, useMemo, useCallback } from 'react';
function ExpensiveCalculation({ list, filter }) {
// 비용이 많이 드는 계산 결과 메모이제이션
const filteredList = useMemo(() => {
console.log('Calculating filtered list...');
return list.filter(item => item.includes(filter));
}, [list, filter]); // list나 filter가 변경될 때만 재계산
// 복잡한 계산 예시
const statistics = useMemo(() => {
console.log('Calculating statistics...');
// 시간이 많이 걸리는 계산 시뮬레이션
const startTime = performance.now();
while (performance.now() - startTime < 100) {
// 인위적인 지연
}
return {
count: filteredList.length,
longestItem: filteredList.reduce(
(max, item) => (item.length > max.length ? item : max),
''
),
averageLength: filteredList.reduce(
(sum, item) => sum + item.length,
0
) / (filteredList.length || 1)
};
}, [filteredList]);
// 이벤트 핸들러 메모이제이션
const handleItemClick = useCallback((item) => {
console.log('Item clicked:', item);
// 처리 로직...
}, []); // 의존성이 없으므로 컴포넌트가 리렌더링되어도 함수는 유지됨
return (
<div>
<h2>Filtered List ({statistics.count} items)</h2>
<p>Longest item: {statistics.longestItem}</p>
<p>Average length: {statistics.averageLength.toFixed(2)} characters</p>
<ul>
{filteredList.map(item => (
<li key={item} onClick={() => handleItemClick(item)}>
{item}
</li>
))}
</ul>
</div>
);
}
// 부모 컴포넌트
function SearchableList() {
const [filter, setFilter] = useState('');
const [list, setList] = useState([
'Apple', 'Banana', 'Cherry', 'Date', 'Elderberry',
'Fig', 'Grape', 'Honeydew', 'Kiwi', 'Lemon'
]);
const [count, setCount] = useState(0);
// useCallback으로 함수 메모이제이션
const addItem = useCallback(() => {
const fruits = ['Mango', 'Nectarine', 'Orange', 'Papaya', 'Quince'];
const randomFruit = fruits[Math.floor(Math.random() * fruits.length)];
setList(prevList => [...prevList, randomFruit]);
}, []);
return (
<div>
<div className="controls">
<input
type="text"
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<button onClick={addItem}>Add Random Fruit</button>
<button onClick={() => setCount(count + 1)}>
Count: {count} (Unrelated State)
</button>
</div>
<ExpensiveCalculation list={list} filter={filter} />
</div>
);
}
코멘트:
useMemo
와useCallback
은 React의 성능 최적화를 위한 핵심 Hook입니다:
useMemo
: 계산 비용이 많이 드는 값을 메모이제이션합니다. 의존성 배열의 값이 변경될 때만 재계산됩니다.useCallback
: 함수를 메모이제이션합니다. 특히 자식 컴포넌트에 props로 전달되는 함수에 유용합니다.이러한 Hook은 다음과 같은 경우에 사용하는 것이 좋습니다:
- 계산 비용이 많이 드는 연산
- 깊은 컴포넌트 트리로 전달되는 함수
React.memo
로 최적화된 컴포넌트에 전달되는 함수나 객체모든 값이나 함수를 메모이제이션하는 것은 오히려 성능을 저하시킬 수 있으므로, 실제 성능 이슈가 있는 경우에만 사용하세요.
가상화 (Virtualization)
import { useState } from 'react';
import { FixedSizeList } from 'react-window';
function VirtualizedList() {
// 대량의 데이터 생성 (예: 10,000개 항목)
const [items] = useState(() =>
Array.from({ length: 10000 }, (_, index) => ({
id: index,
text: `Item ${index}`,
description: `This is the description for item ${index}`
}))
);
// 각 항목 렌더링 함수
const Row = ({ index, style }) => {
const item = items[index];
return (
<div
style=
>
<div>
<h3>{item.text}</h3>
<p>{item.description}</p>
</div>
</div>
);
};
return (
<div>
<h2>Virtualized List (10,000 items)</h2>
<p>Only renders items visible in the viewport</p>
{/* 가상화된 리스트 */}
<FixedSizeList
height={400}
width="100%"
itemCount={items.length}
itemSize={80} // 각 항목의 높이
>
{Row}
</FixedSizeList>
</div>
);
}
// 그리드 가상화 예시
import { FixedSizeGrid } from 'react-window';
function VirtualizedGrid() {
const columnCount = 100;
const rowCount = 100;
// 셀 렌더링 함수
const Cell = ({ columnIndex, rowIndex, style }) => (
<div
style=
>
r{rowIndex}, c{columnIndex}
</div>
);
return (
<div>
<h2>Virtualized Grid (100x100 cells)</h2>
<FixedSizeGrid
columnCount={columnCount}
columnWidth={100}
height={400}
rowCount={rowCount}
rowHeight={35}
width={500}
>
{Cell}
</FixedSizeGrid>
</div>
);
}
코멘트: 가상화(Virtualization)는 대량의 데이터를 효율적으로 렌더링하는 기술로, 화면에 보이는 요소만 실제로 렌더링합니다.
react-window
나react-virtualized
같은 라이브러리를 사용하여 구현할 수 있습니다. 이 기술은 다음과 같은 경우에 특히 유용합니다:
- 수천 개의 항목을 포함하는 긴 목록
- 대규모 테이블이나 그리드
- 무한 스크롤 UI
가상화를 사용하면 메모리 사용량을 줄이고 렌더링 성능을 크게 향상시킬 수 있습니다. 모바일 기기와 같은 저사양 환경에서 특히 중요합니다.
코드 분할 (Code Splitting)
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
// 일반적인 정적 import
import Home from './Home';
// 지연 로딩을 위한 동적 import
const About = lazy(() => import('./About'));
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/dashboard">Dashboard</Link></li>
<li><Link to="/settings">Settings</Link></li>
</ul>
</nav>
{/* Suspense는 지연 로딩 컴포넌트가 로드되는 동안 fallback UI를 표시 */}
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</div>
</Router>
);
}
// 컴포넌트 내부에서 조건부 지연 로딩
function ProductPage({ productId }) {
const ProductDetails = lazy(() => import('./ProductDetails'));
const ProductReviews = lazy(() => import('./ProductReviews'));
const [activeTab, setActiveTab] = useState('details');
return (
<div>
<h1>Product {productId}</h1>
<div className="tabs">
<button
onClick={() => setActiveTab('details')}
className={activeTab === 'details' ? 'active' : ''}
>
Details
</button>
<button
onClick={() => setActiveTab('reviews')}
className={activeTab === 'reviews' ? 'active' : ''}
>
Reviews
</button>
</div>
<Suspense fallback={<div>Loading tab...</div>}>
{activeTab === 'details' ? (
<ProductDetails id={productId} />
) : (
<ProductReviews id={productId} />
)}
</Suspense>
</div>
);
}
코멘트: 코드 분할(Code Splitting)은 애플리케이션을 더 작은 청크로 나누어 필요할 때만 로드하는 기술입니다. React에서는
React.lazy
와Suspense
를 사용하여 구현합니다. 이 기술의 장점은 다음과 같습니다:
- 초기 로딩 시간 단축
- 필요한 코드만 로드하여 리소스 절약
- 사용자가 실제로 방문하는 페이지만 다운로드
코드 분할은 다음과 같은 경우에 특히 유용합니다:
- 라우트 기반 분할 (각 페이지별로 분할)
- 컴포넌트 기반 분할 (큰 컴포넌트나 라이브러리)
- 조건부 로딩 (특정 조건에서만 필요한 기능)
불필요한 렌더링 방지
import { useState, useEffect, useRef } from 'react';
function RenderCounter({ id }) {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<div className="render-counter">
Component {id} rendered {renderCount.current} times
</div>
);
}
// 불필요한 렌더링이 발생하는 예
function IneffectiveComponent() {
const [count, setCount] = useState(0);
// 매 렌더링마다 새로운 함수 생성
const handleClick = () => {
console.log('Button clicked');
};
// 매 렌더링마다 새로운 객체 생성
const user = { name: 'John', age: 30 };
return (
<div>
<h2>Ineffective Component <RenderCounter id="ineffective" /></h2>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* 매 렌더링마다 새로운 props를 받아 리렌더링됨 */}
<ChildComponent user={user} onClick={handleClick} />
</div>
);
}
// 최적화된 버전
function OptimizedComponent() {
const [count, setCount] = useState(0);
// useCallback으로 함수 메모이제이션
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
// useMemo로 객체 메모이제이션
const user = useMemo(() => ({ name: 'John', age: 30 }), []);
return (
<div>
<h2>Optimized Component <RenderCounter id="optimized" /></h2>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* 메모이제이션된 props로 불필요한 리렌더링 방지 */}
<MemoizedChildComponent user={user} onClick={handleClick} />
</div>
);
}
// React.memo로 최적화된 자식 컴포넌트
const MemoizedChildComponent = memo(function ChildComponent({ user, onClick }) {
return (
<div>
<RenderCounter id="child" />
<p>User: {user.name}, {user.age}</p>
<button onClick={onClick}>Click Me</button>
</div>
);
});
// 일반 자식 컴포넌트
function ChildComponent({ user, onClick }) {
return (
<div>
<RenderCounter id="child" />
<p>User: {user.name}, {user.age}</p>
<button onClick={onClick}>Click Me</button>
</div>
);
}
코멘트: React에서 불필요한 렌더링을 방지하는 것은 성능 최적화의 핵심입니다. 주요 전략은 다음과 같습니다:
React.memo
를 사용하여 props가 변경되지 않으면 컴포넌트 리렌더링 방지useMemo
를 사용하여 객체, 배열, 계산 결과 메모이제이션useCallback
을 사용하여 이벤트 핸들러 함수 메모이제이션- 상태 업데이트 로직 최적화 (불필요한 상태 업데이트 방지)
- 컴포넌트 구조 재설계 (상태를 적절한 위치에 배치)
렌더링 최적화는 항상 측정 가능한 성능 문제가 있을 때만 적용해야 합니다. 과도한 최적화는 코드를 복잡하게 만들 수 있습니다.
Leave a comment