레슨 11 / 12·6개 토픽
Error Boundary와 성능 최적화
Error Boundary란?
Error Boundary는 하위 컴포넌트 트리에서 발생하는 JavaScript 에러를 잡아내고, 에러 UI를 표시하는 React 패턴입니다. 현재 클래스 컴포넌트로만 구현할 수 있으며, react-error-boundary 라이브러리로 더 편리하게 사용합니다.
클래스 기반 Error Boundary
tsx
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
// 에러 리포팅 서비스로 전송 가능
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<h2 className="text-red-600">문제가 발생했습니다.</h2>
<p className="text-red-500">{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
다시 시도
</button>
</div>
);
}
return this.props.children;
}
}
// 사용법
function App() {
return (
<ErrorBoundary fallback={<p>에러 발생!</p>}>
<MyComponent />
</ErrorBoundary>
);
}react-error-boundary 라이브러리
react-error-boundary 패키지를 사용하면 함수형 컴포넌트 스타일로 더 간편하게 Error Boundary를 구현할 수 있습니다. 재시도, 에러 리셋, fallback 커스터마이징 등 다양한 기능을 제공합니다.
tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div className="p-4 bg-red-50 rounded">
<h2>오류가 발생했습니다</h2>
<pre className="text-sm text-red-600">{error.message}</pre>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 에러 복구 시 실행할 로직 (상태 초기화 등)
}}
onError={(error, info) => {
// 에러 리포팅 서비스로 전송
console.error('Logged error:', error, info);
}}
>
<Dashboard />
</ErrorBoundary>
);
}React.memo로 리렌더링 방지
React.memo는 컴포넌트의 props가 변경되지 않으면 리렌더링을 건너뛰는 고차 컴포넌트(HOC)입니다. 렌더링 비용이 큰 컴포넌트에 적용하면 성능을 크게 개선할 수 있습니다.
tsx
import { memo, useState, useCallback } from 'react';
// memo로 감싸면 props가 같을 때 리렌더링 건너뜀
const ExpensiveList = memo(function ExpensiveList({
items,
onSelect,
}: {
items: string[];
onSelect: (item: string) => void;
}) {
console.log('ExpensiveList 렌더링');
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => onSelect(item)}>
{item}
</li>
))}
</ul>
);
});
function Parent() {
const [query, setQuery] = useState('');
const [items] = useState(['React', 'Vue', 'Angular', 'Svelte']);
// useCallback으로 함수 참조 안정화 — memo와 함께 사용
const handleSelect = useCallback((item: string) => {
console.log('선택:', item);
}, []);
return (
<div>
{/* query가 변경되어도 ExpensiveList는 리렌더링되지 않음 */}
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="검색..." />
<ExpensiveList items={items} onSelect={handleSelect} />
</div>
);
}React.lazy와 Suspense
React.lazy를 사용하면 컴포넌트를 동적으로 import하여 코드 스플리팅을 적용할 수 있습니다. Suspense와 함께 사용하면 로딩 중 대체 UI를 표시합니다.
tsx
import { lazy, Suspense } from 'react';
// 동적 import — 해당 컴포넌트가 필요할 때만 로드
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const AdminPanel = lazy(() => import('./components/AdminPanel'));
function App() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>대시보드</h1>
<button onClick={() => setShowChart(true)}>차트 보기</button>
{/* 로딩 중 fallback UI 표시 */}
<Suspense fallback={<div className="animate-pulse p-4 bg-gray-100 rounded">로딩 중...</div>}>
{showChart && <HeavyChart />}
</Suspense>
{/* 여러 lazy 컴포넌트를 하나의 Suspense로 감쌀 수 있음 */}
<Suspense fallback={<p>관리 패널 로딩 중...</p>}>
<AdminPanel />
</Suspense>
</div>
);
}리스트와 key 최적화
React에서 리스트를 렌더링할 때 key prop은 각 요소의 고유 식별자입니다. 올바른 key를 사용해야 React가 변경된 요소만 효율적으로 업데이트할 수 있습니다.
- •고유한 ID 사용 — 배열 인덱스 대신 데이터의 고유 ID를 key로 사용
- •인덱스 key 지양 — 순서가 변경되면 불필요한 리렌더링과 버그 발생
- •key 변경 = 재마운트 — key를 의도적으로 변경하면 컴포넌트를 완전히 재생성
tsx
// ✅ 올바른 예: 고유 ID를 key로 사용
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
// ❌ 잘못된 예: 인덱스를 key로 사용 (순서 변경 시 문제)
{users.map((user, index) => (
<UserCard key={index} user={user} />
))}
// 💡 key 변경으로 컴포넌트 강제 재마운트
// userId가 바뀌면 Profile이 완전히 새로 생성됨
<Profile key={userId} userId={userId} />💡
성능 최적화는 실제 성능 문제가 관측될 때 적용하세요. React DevTools의 Profiler를 사용하면 어떤 컴포넌트가 불필요하게 리렌더링되는지 확인할 수 있습니다. 조기 최적화는 코드 복잡도만 높입니다.