컴포넌트 최적화
2. 컴포넌트 최적화
React 애플리케이션의 성능을 향상시키기 위한 다양한 최적화 기법을 배워보겠습니다. React.memo, useMemo, useCallback 등을 활용해서 불필요한 렌더링을 방지하고 애플리케이션을 더 빠르게 만드는 방법을 알아봅시다.
2.1 React.memo로 컴포넌트 메모이제이션
기본 React.memo 사용법
import { useState, memo } from 'react';
// 최적화되지 않은 컴포넌트
function ExpensiveComponent({ name, count }) {
console.log(`ExpensiveComponent 렌더링됨: ${name}`);
// 복잡한 계산 시뮬레이션
const expensiveValue = (() => {
let result = 0;
for (let i = 0; i < 100000; i++) {
result += Math.random();
}
return result.toFixed(2);
})();
return (
<div >
<h3>{name}</h3>
<p>카운트: {count}</p>
<p>복잡한 계산 결과: {expensiveValue}</p>
</div>
);
}
// React.memo로 최적화된 컴포넌트
const OptimizedComponent = memo(function OptimizedComponent({ name, count }) {
console.log(`OptimizedComponent 렌더링됨: ${name}`);
const expensiveValue = (() => {
let result = 0;
for (let i = 0; i < 100000; i++) {
result += Math.random();
}
return result.toFixed(2);
})();
return (
<div >
<h3>{name}</h3>
<p>카운트: {count}</p>
<p>복잡한 계산 결과: {expensiveValue}</p>
</div>
);
});
function MemoExample() {
const [parentCount, setParentCount] = useState(0);
const [childCount, setChildCount] = useState(0);
return (
<div }>
<h2>React.memo 비교 예제</h2>
<div }>
<button
onClick={() => setParentCount(prev => prev + 1)}
>
부모 상태 변경 ({parentCount})
</button>
<button
onClick={() => setChildCount(prev => prev + 1)}
>
자식 카운트 변경 ({childCount})
</button>
</div>
<div }>
<div>
<h3>최적화되지 않은 컴포넌트</h3>
<ExpensiveComponent name="컴포넌트 1" count={childCount} />
</div>
<div>
<h3>React.memo로 최적화된 컴포넌트</h3>
<OptimizedComponent name="컴포넌트 2" count={childCount} />
</div>
</div>
<div >
<h4>💡 테스트 방법</h4>
<ol }>
<li>개발자 도구 콘솔을 열어주세요</li>
<li>"부모 상태 변경" 버튼을 클릭해보세요</li>
<li>파란색(최적화되지 않은) 컴포넌트는 매번 렌더링되지만</li>
<li>녹색(최적화된) 컴포넌트는 props가 변경되지 않아 렌더링되지 않습니다</li>
</ol>
</div>
</div>
);
}
커스텀 비교 함수
import { useState, memo } from 'react';
const UserCard = memo(function UserCard({ user, theme }) {
console.log(`UserCard 렌더링됨: ${user.name}`);
return (
<div >
<h3>{user.name}</h3>
<p>나이: {user.age}</p>
<p>이메일: {user.email}</p>
<p>마지막 로그인: {user.lastLogin}</p>
</div>
);
});
// 커스텀 비교 함수를 사용한 최적화
const OptimizedUserCard = memo(function OptimizedUserCard({ user, theme }) {
console.log(`OptimizedUserCard 렌더링됨: ${user.name}`);
return (
<div >
<h3>{user.name} ⚡</h3>
<p>나이: {user.age}</p>
<p>이메일: {user.email}</p>
<p>마지막 로그인: {user.lastLogin}</p>
</div>
);
}, (prevProps, nextProps) => {
// lastLogin을 제외하고 비교 (자주 변경되는 값 무시)
return (
prevProps.user.name === nextProps.user.name &&
prevProps.user.age === nextProps.user.age &&
prevProps.user.email === nextProps.user.email &&
prevProps.theme === nextProps.theme
);
});
function CustomComparisonExample() {
const [theme, setTheme] = useState('light');
const [updateCount, setUpdateCount] = useState(0);
const user = {
name: '김개발',
age: 25,
email: 'kim@example.com',
lastLogin: new Date().toLocaleString() // 매번 변경되는 값
};
return (
<div }>
<h2>커스텀 비교 함수 예제</h2>
<div }>
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
테마 변경 ({theme})
</button>
<button
onClick={() => setUpdateCount(prev => prev + 1)}
>
강제 업데이트 ({updateCount})
</button>
</div>
<div }>
<div>
<h3>기본 React.memo</h3>
<UserCard user={user} theme={theme} />
</div>
<div>
<h3>커스텀 비교 함수</h3>
<OptimizedUserCard user={user} theme={theme} />
</div>
</div>
<div >
<h4>🔍 동작 원리</h4>
<ul }>
<li><strong>기본 React.memo:</strong> lastLogin이 매번 변경되어 항상 재렌더링</li>
<li><strong>커스텀 비교:</strong> lastLogin을 무시하고 중요한 props만 비교</li>
<li><strong>성능 향상:</strong> 불필요한 렌더링 방지로 성능 개선</li>
</ul>
</div>
</div>
);
}
2.2 useMemo로 값 메모이제이션
복잡한 계산 최적화
import { useState, useMemo } from 'react';
function ExpensiveCalculation({ items, multiplier }) {
console.log('ExpensiveCalculation 컴포넌트 렌더링');
// useMemo 없이 매번 계산
const expensiveValue = (() => {
console.log('복잡한 계산 실행 중...');
let result = 0;
// 복잡한 계산 시뮬레이션
for (let i = 0; i < items.length; i++) {
for (let j = 0; j < 1000000; j++) {
result += items[i].value * multiplier;
}
}
return result;
})();
// useMemo로 최적화된 계산
const optimizedValue = useMemo(() => {
console.log('최적화된 계산 실행 중...');
let result = 0;
for (let i = 0; i < items.length; i++) {
for (let j = 0; j < 1000000; j++) {
result += items[i].value * multiplier;
}
}
return result;
}, [items, multiplier]); // items나 multiplier가 변경될 때만 재계산
return (
<div >
<div >
<h3>❌ 최적화 없음</h3>
<p>계산 결과: {expensiveValue.toLocaleString()}</p>
<small>매번 계산 실행</small>
</div>
<div >
<h3>✅ useMemo 사용</h3>
<p>계산 결과: {optimizedValue.toLocaleString()}</p>
<small>의존성 변경시만 계산</small>
</div>
</div>
);
}
function MemoExample() {
const [items, setItems] = useState([
{ id: 1, value: 10 },
{ id: 2, value: 20 },
{ id: 3, value: 30 }
]);
const [multiplier, setMultiplier] = useState(2);
const [unrelatedState, setUnrelatedState] = useState(0);
const addItem = () => {
const newItem = {
id: items.length + 1,
value: Math.floor(Math.random() * 50) + 1
};
setItems(prev => [...prev, newItem]);
};
const removeItem = (id) => {
setItems(prev => prev.filter(item => item.id !== id));
};
return (
<div }>
<h2>useMemo 최적화 예제</h2>
<div }>
<button
onClick={addItem}
>
아이템 추가
</button>
<button
onClick={() => setMultiplier(prev => prev + 1)}
>
배수 증가 ({multiplier})
</button>
<button
onClick={() => setUnrelatedState(prev => prev + 1)}
>
관련없는 상태 변경 ({unrelatedState})
</button>
</div>
<div }>
<h3>아이템 목록</h3>
<div }>
{items.map(item => (
<div
key={item.id}
>
<span>값: {item.value}</span>
<button
onClick={() => removeItem(item.id)}
>
×
</button>
</div>
))}
</div>
</div>
<ExpensiveCalculation items={items} multiplier={multiplier} />
<div >
<h4>🧪 테스트해보기</h4>
<ol }>
<li>개발자 도구 콘솔을 열어주세요</li>
<li>"관련없는 상태 변경" 버튼을 클릭해보세요</li>
<li>최적화되지 않은 계산은 매번 실행되지만, useMemo는 실행되지 않습니다</li>
<li>"아이템 추가"나 "배수 증가"를 클릭하면 둘 다 다시 계산됩니다</li>
</ol>
</div>
</div>
);
}
2.3 useCallback으로 함수 메모이제이션
함수 참조 최적화
import { useState, useCallback, memo } from 'react';
// 자식 컴포넌트 - 함수를 prop으로 받음
const ChildComponent = memo(function ChildComponent({ name, onClick }) {
console.log(`ChildComponent 렌더링: ${name}`);
return (
<div >
<h4>{name}</h4>
<button
onClick={onClick}
>
클릭하세요
</button>
</div>
);
});
function CallbackExample() {
const [count, setCount] = useState(0);
const [unrelatedState, setUnrelatedState] = useState(0);
// useCallback 없는 함수 - 매번 새로운 함수 생성
const handleClick1 = () => {
setCount(prev => prev + 1);
};
// useCallback으로 최적화된 함수 - 의존성 배열이 비어있으므로 한 번만 생성
const handleClick2 = useCallback(() => {
setCount(prev => prev + 1);
}, []);
// 의존성이 있는 useCallback
const handleClick3 = useCallback(() => {
setCount(count + 1);
}, [count]);
// 복잡한 함수 최적화
const handleComplexOperation = useCallback((multiplier) => {
return (value) => {
console.log(`복잡한 연산 실행: ${value} × ${multiplier}`);
return value * multiplier;
};
}, []);
return (
<div }>
<h2>useCallback 최적화 예제</h2>
<div }>
<p>현재 카운트: <strong>{count}</strong></p>
<button
onClick={() => setUnrelatedState(prev => prev + 1)}
>
관련없는 상태 변경 ({unrelatedState})
</button>
</div>
<div }>
<div>
<h3>❌ useCallback 없음</h3>
<ChildComponent
name="컴포넌트 1"
onClick={handleClick1}
/>
</div>
<div>
<h3>✅ useCallback 사용</h3>
<ChildComponent
name="컴포넌트 2"
onClick={handleClick2}
/>
</div>
<div>
<h3>⚠️ 의존성 있는 useCallback</h3>
<ChildComponent
name="컴포넌트 3"
onClick={handleClick3}
/>
</div>
</div>
<div >
<h4>🔍 동작 원리</h4>
<ul }>
<li><strong>컴포넌트 1:</strong> 매번 새로운 함수가 생성되어 항상 재렌더링</li>
<li><strong>컴포넌트 2:</strong> 함수가 메모이제이션되어 불필요한 렌더링 방지</li>
<li><strong>컴포넌트 3:</strong> count 의존성으로 인해 count 변경시 재렌더링</li>
</ul>
</div>
</div>
);
}
실전 useCallback 활용
import { useState, useCallback, useMemo } from 'react';
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete, onEdit }) {
console.log(`TodoItem 렌더링: ${todo.id}`);
return (
<div >
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
}
/>
<span
>
{todo.text}
</span>
<div }>
<button
onClick={() => onEdit(todo.id, prompt('새 텍스트:', todo.text))}
>
수정
</button>
<button
onClick={() => onDelete(todo.id)}
>
삭제
</button>
</div>
</div>
);
});
function OptimizedTodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React 공부하기', completed: false },
{ id: 2, text: 'useState 이해하기', completed: true },
{ id: 3, text: '컴포넌트 최적화하기', completed: false }
]);
const [filter, setFilter] = useState('all'); // all, active, completed
const [newTodo, setNewTodo] = useState('');
// useCallback으로 최적화된 이벤트 핸들러들
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
const handleEdit = useCallback((id, newText) => {
if (newText && newText.trim()) {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText.trim() } : todo
));
}
}, []);
const handleAdd = useCallback(() => {
if (newTodo.trim()) {
const newId = Math.max(...todos.map(t => t.id), 0) + 1;
setTodos(prev => [...prev, {
id: newId,
text: newTodo.trim(),
completed: false
}]);
setNewTodo('');
}
}, [newTodo, todos]);
// useMemo로 필터링된 todos 최적화
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
// useMemo로 통계 정보 최적화
const stats = useMemo(() => {
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
};
}, [todos]);
return (
<div >
<h2>최적화된 할 일 앱</h2>
{/* 새 할 일 추가 */}
<div >
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
placeholder="새로운 할 일을 입력하세요"
/>
<button
onClick={handleAdd}
>
추가
</button>
</div>
{/* 필터 버튼들 */}
<div >
{['all', 'active', 'completed'].map(filterType => (
<button
key={filterType}
onClick={() => setFilter(filterType)}
>
{filterType === 'all' ? '전체' :
filterType === 'active' ? '미완료' : '완료'}
</button>
))}
</div>
{/* 통계 정보 */}
<div >
<div }>
<div }>
{stats.total}
</div>
<div }>전체</div>
</div>
<div }>
<div }>
{stats.completed}
</div>
<div }>완료</div>
</div>
<div }>
<div }>
{stats.active}
</div>
<div }>미완료</div>
</div>
</div>
{/* 할 일 목록 */}
<div>
{filteredTodos.length === 0 ? (
<div >
{filter === 'all' ? '할 일이 없습니다.' :
filter === 'active' ? '미완료된 할 일이 없습니다.' :
'완료된 할 일이 없습니다.'}
</div>
) : (
filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))
)}
</div>
<div >
<h4>✨ 적용된 최적화 기법</h4>
<ul }>
<li><strong>React.memo:</strong> TodoItem 컴포넌트 불필요한 렌더링 방지</li>
<li><strong>useCallback:</strong> 이벤트 핸들러 함수 메모이제이션</li>
<li><strong>useMemo:</strong> 필터링된 todos와 통계 정보 캐싱</li>
<li><strong>키 최적화:</strong> 안정적인 key 사용으로 리스트 렌더링 최적화</li>
</ul>
</div>
</div>
);
}
2.4 성능 측정과 프로파일링
React DevTools Profiler 활용 팁
import { useState, Profiler } from 'react';
function ExpensiveComponentTree() {
const [count, setCount] = useState(0);
// Profiler 콜백 함수
const onRenderCallback = (id, phase, actualDuration) => {
console.log('Profiler 측정 결과:', {
컴포넌트: id,
단계: phase, // mount, update
렌더링시간: `${actualDuration}ms`
});
};
return (
<div }>
<h2>성능 프로파일링 예제</h2>
<button
onClick={() => setCount(prev => prev + 1)}
>
카운트 증가 ({count})
</button>
<Profiler id="ExpensiveTree" onRender={onRenderCallback}>
<div>
<h3>무거운 컴포넌트 트리</h3>
{Array.from({ length: 100 }, (_, i) => (
<div
key={i}
>
아이템 {i + 1} - 카운트: {count}
</div>
))}
</div>
</Profiler>
<div >
<h4>📊 성능 측정 도구</h4>
<ul }>
<li><strong>React DevTools Profiler:</strong> 컴포넌트 렌더링 시간 측정</li>
<li><strong>브라우저 Performance 탭:</strong> 전체 성능 분석</li>
<li><strong>console.time/timeEnd:</strong> 특정 코드 실행 시간 측정</li>
</ul>
</div>
</div>
);
}
2.5 최적화 주의사항과 Best Practices
언제 최적화를 적용해야 할까?
import { useState, memo, useMemo, useCallback } from 'react';
// ❌ 과도한 최적화 - 불필요함
const OverOptimizedComponent = memo(function OverOptimizedComponent({ text }) {
const processedText = useMemo(() => {
return text.toUpperCase(); // 간단한 연산까지 메모이제이션
}, [text]);
const handleClick = useCallback(() => {
console.log('클릭됨'); // 간단한 함수까지 메모이제이션
}, []);
return (
<div onClick={handleClick}>
{processedText}
</div>
);
});
// ✅ 적절한 최적화 - 필요한 곳에만
function WellOptimizedComponent({ items, onItemClick }) {
// 복잡한 계산만 useMemo 사용
const expensiveCalculation = useMemo(() => {
return items.reduce((acc, item) => {
// 복잡한 로직
return acc + Math.pow(item.value, 2);
}, 0);
}, [items]);
// 자식 컴포넌트에 전달되는 함수만 useCallback 사용
const handleItemClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
<h3>계산 결과: {expensiveCalculation}</h3>
{items.map(item => (
<ExpensiveChildComponent
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</div>
);
}
const ExpensiveChildComponent = memo(function ExpensiveChildComponent({ item, onClick }) {
// 복잡한 렌더링 로직이 있는 경우에만 memo 사용
return (
<div
onClick={() => onClick(item.id)}
>
{item.name} - {item.description}
</div>
);
});
function OptimizationBestPractices() {
const [items] = useState([
{ id: 1, name: '아이템 1', description: '설명 1', value: 10 },
{ id: 2, name: '아이템 2', description: '설명 2', value: 20 },
{ id: 3, name: '아이템 3', description: '설명 3', value: 30 }
]);
const handleItemClick = (id) => {
console.log('아이템 클릭:', id);
};
return (
<div }>
<h2>최적화 Best Practices</h2>
<div }>
<WellOptimizedComponent
items={items}
onItemClick={handleItemClick}
/>
</div>
<div >
<h3>🎯 최적화 가이드라인</h3>
<h4>언제 React.memo를 사용할까?</h4>
<ul }>
<li>✅ 컴포넌트가 자주 렌더링되고 props가 자주 바뀌지 않는 경우</li>
<li>✅ 렌더링 비용이 높은 컴포넌트</li>
<li>❌ props가 매번 바뀌는 컴포넌트</li>
<li>❌ 렌더링 비용이 낮은 간단한 컴포넌트</li>
</ul>
<h4>언제 useMemo를 사용할까?</h4>
<ul }>
<li>✅ 복잡한 계산이나 데이터 변환</li>
<li>✅ 큰 배열의 필터링이나 정렬</li>
<li>✅ 객체나 배열을 새로 생성하는 경우</li>
<li>❌ 간단한 계산 (문자열 연결, 기본 산술 연산)</li>
</ul>
<h4>언제 useCallback을 사용할까?</h4>
<ul }>
<li>✅ 자식 컴포넌트의 props로 전달되는 함수</li>
<li>✅ useEffect의 의존성으로 사용되는 함수</li>
<li>✅ 다른 훅의 의존성 배열에 포함되는 함수</li>
<li>❌ 컴포넌트 내부에서만 사용되는 간단한 함수</li>
</ul>
<h4>⚠️ 주의사항</h4>
<ul }>
<li>측정하지 않은 최적화는 조기 최적화가 될 수 있음</li>
<li>React DevTools Profiler로 실제 성능 문제를 확인 후 적용</li>
<li>의존성 배열을 정확히 명시해야 함</li>
<li>과도한 메모이제이션은 오히려 메모리를 낭비할 수 있음</li>
</ul>
</div>
</div>
);
}
2.6 정리
React 컴포넌트 최적화의 핵심 기법들을 학습했습니다:
- React.memo로 불필요한 컴포넌트 렌더링 방지
- useMemo로 복잡한 계산 결과 캐싱
- useCallback으로 함수 참조 최적화
- 성능 측정을 통한 최적화 포인트 파악
- Best Practices와 적절한 최적화 시점 판단
최적화는 성능 문제가 실제로 발생했을 때 적용하는 것이 좋습니다. 모든 컴포넌트를 무분별하게 최적화하는 것보다는, React DevTools Profiler 등을 활용해서 실제 성능 병목지점을 찾아 집중적으로 최적화하는 것이 효과적입니다.
다음 장에서는 React의 State와 이벤트 처리에 대해 더 깊이 알아보겠습니다.
export { MemoExample, CustomComparisonExample, CallbackExample, OptimizedTodoApp, ExpensiveComponentTree, OptimizationBestPractices };