에러 바운더리
4. 에러 바운더리
에러 바운더리(Error Boundary)는 React 애플리케이션에서 JavaScript 오류를 잡아내고, 오류 정보를 기록하며, 오류가 발생한 컴포넌트 대신 대체 UI를 표시하는 React 컴포넌트입니다. 애플리케이션 전체가 크래시되는 것을 방지할 수 있습니다.
에러 바운더리가 필요한 이유
React 16 이전과 이후의 차이점
React 16 이전에는 컴포넌트 내부의 JavaScript 오류가 React의 내부 상태를 손상시켜 다음 렌더링에서 알 수 없는 오류를 발생시켰습니다. React 16부터는 오류가 발생하면 전체 컴포넌트 트리를 언마운트합니다.
// 에러가 발생할 수 있는 컴포넌트
function ProblematicComponent({ user }) {
// user가 null이면 오류 발생
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function App() {
const [user, setUser] = useState(null);
return (
<div>
<h1>내 애플리케이션</h1>
<ProblematicComponent user={user} />
<button onClick={() => setUser({ name: '김개발', email: 'kim@example.com' })}>
사용자 설정
</button>
</div>
);
}
위 코드에서 user가 null인 상태에서 ProblematicComponent가 렌더링되면 오류가 발생하고 전체 앱이 크래시됩니다.
클래스 컴포넌트로 에러 바운더리 구현
기본 에러 바운더리
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// 에러가 발생했을 때 state를 업데이트
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// 에러 정보를 기록
componentDidCatch(error, errorInfo) {
console.error('에러 바운더리가 에러를 포착했습니다:', error);
console.error('에러 정보:', errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>문제가 발생했습니다</h2>
<p>죄송합니다. 예기치 못한 오류가 발생했습니다.</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
다시 시도
</button>
</div>
);
}
return this.props.children;
}
}
에러 바운더리 사용하기
function SafeApp() {
const [user, setUser] = useState(null);
return (
<div>
<h1>안전한 애플리케이션</h1>
<ErrorBoundary>
<ProblematicComponent user={user} />
</ErrorBoundary>
<button onClick={() => setUser({ name: '김개발', email: 'kim@example.com' })}>
사용자 설정
</button>
<button onClick={() => setUser(null)}>
사용자 제거 (오류 발생)
</button>
</div>
);
}
고급 에러 바운더리
상세한 에러 정보를 포함한 에러 바운더리
import React from 'react';
class DetailedErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error: error,
errorInfo: errorInfo
});
// 에러 로깅 서비스에 전송 (예: Sentry, LogRocket)
console.error('에러 발생:', error);
console.error('컴포넌트 스택:', errorInfo.componentStack);
}
render() {
if (this.state.hasError) {
const { fallback: Fallback } = this.props;
// 커스텀 fallback 컴포넌트가 있으면 사용
if (Fallback) {
return (
<Fallback
error={this.state.error}
errorInfo={this.state.errorInfo}
resetError={() => this.setState({ hasError: false, error: null, errorInfo: null })}
/>
);
}
// 기본 에러 UI
return (
<div>
<h2>오류가 발생했습니다</h2>
<details>
<summary>에러 상세 정보 (개발 모드)</summary>
<pre>{this.state.error && this.state.error.toString()}</pre>
<pre>{this.state.errorInfo.componentStack}</pre>
</details>
<button onClick={() => this.setState({ hasError: false, error: null, errorInfo: null })}>
다시 시도
</button>
</div>
);
}
return this.props.children;
}
}
// 커스텀 에러 UI 컴포넌트
function CustomErrorFallback({ error, resetError }) {
return (
<div role="alert">
<h2>앗! 무언가 잘못되었습니다</h2>
<pre>{error.message}</pre>
<button onClick={resetError}>다시 시도</button>
</div>
);
}
// 사용 예제
function AppWithCustomError() {
return (
<DetailedErrorBoundary fallback={CustomErrorFallback}>
<ProblematicComponent user={null} />
</DetailedErrorBoundary>
);
}
함수형 컴포넌트와 함께 사용하기
react-error-boundary 라이브러리 스타일
import React from 'react';
// 에러 바운더리 Hook (실제 Hook은 아니지만 Hook 스타일의 API)
function withErrorBoundary(Component, ErrorComponent) {
return function WithErrorBoundaryComponent(props) {
return (
<ErrorBoundary fallback={ErrorComponent}>
<Component {...props} />
</ErrorBoundary>
);
};
}
// 함수형 컴포넌트에서 에러 바운더리 사용
const SafeProblematicComponent = withErrorBoundary(
ProblematicComponent,
function ErrorFallback({ error, resetError }) {
return (
<div>
<h3>컴포넌트에서 오류가 발생했습니다</h3>
<p>{error?.message}</p>
<button onClick={resetError}>다시 시도</button>
</div>
);
}
);
에러 바운더리가 포착하지 못하는 오류들
에러 바운더리의 한계
function ErrorExamples() {
const [asyncError, setAsyncError] = useState(false);
// ❌ 에러 바운더리가 포착하지 못하는 오류들
// 1. 이벤트 핸들러 내의 오류
const handleClick = () => {
throw new Error('이벤트 핸들러 오류'); // 포착되지 않음
};
// 2. 비동기 코드의 오류
const handleAsyncError = () => {
setTimeout(() => {
throw new Error('비동기 오류'); // 포착되지 않음
}, 1000);
};
// 3. Server-side rendering 오류
// 4. 에러 바운더리 자체의 오류
return (
<div>
<button onClick={handleClick}>
이벤트 핸들러 오류 (포착되지 않음)
</button>
<button onClick={handleAsyncError}>
비동기 오류 (포착되지 않음)
</button>
</div>
);
}
// 이벤트 핸들러 오류 처리 방법
function SafeEventHandler() {
const [error, setError] = useState(null);
const handleClick = () => {
try {
// 오류가 발생할 수 있는 코드
throw new Error('의도적인 오류');
} catch (err) {
setError(err.message);
}
};
if (error) {
return (
<div>
<p>오류: {error}</p>
<button onClick={() => setError(null)}>다시 시도</button>
</div>
);
}
return (
<button onClick={handleClick}>
안전한 이벤트 핸들러
</button>
);
}
실습: 컴포넌트별 에러 처리
여러 레벨의 에러 바운더리
import React from 'react';
import { useState } from 'react';
// 페이지 레벨 에러 바운더리
class PageErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('페이지 레벨 오류:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>페이지를 불러올 수 없습니다</h1>
<p>전체 페이지에 문제가 발생했습니다.</p>
<button onClick={() => window.location.reload()}>
페이지 새로고침
</button>
</div>
);
}
return this.props.children;
}
}
// 섹션 레벨 에러 바운더리
class SectionErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('섹션 레벨 오류:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h3>이 섹션을 불러올 수 없습니다</h3>
<p>다른 기능은 정상적으로 작동합니다.</p>
<button onClick={() => this.setState({ hasError: false })}>
다시 시도
</button>
</div>
);
}
return this.props.children;
}
}
// 오류가 발생할 수 있는 컴포넌트들
function UserProfile({ userId }) {
if (!userId) {
throw new Error('사용자 ID가 필요합니다');
}
return (
<div>
<h3>사용자 프로필</h3>
<p>사용자 ID: {userId}</p>
</div>
);
}
function ProductList({ products }) {
if (!Array.isArray(products)) {
throw new Error('상품 목록이 배열이 아닙니다');
}
return (
<div>
<h3>상품 목록</h3>
{products.map(product => (
<div key={product.id}>
<p>{product.name}: {product.price}원</p>
</div>
))}
</div>
);
}
function RecommendedProducts() {
// 50% 확률로 오류 발생
if (Math.random() > 0.5) {
throw new Error('추천 상품을 불러올 수 없습니다');
}
return (
<div>
<h3>추천 상품</h3>
<p>오늘의 추천 상품입니다.</p>
</div>
);
}
// 메인 앱 컴포넌트
function ErrorHandlingApp() {
const [userId, setUserId] = useState(null);
const [products, setProducts] = useState([]);
return (
<PageErrorBoundary>
<div>
<h1>쇼핑 앱</h1>
<div>
<button onClick={() => setUserId('user123')}>
유효한 사용자 설정
</button>
<button onClick={() => setUserId(null)}>
사용자 제거 (오류 발생)
</button>
</div>
<div>
<button onClick={() => setProducts([
{ id: 1, name: '노트북', price: 1500000 },
{ id: 2, name: '마우스', price: 50000 }
])}>
상품 목록 설정
</button>
<button onClick={() => setProducts('invalid')}>
잘못된 상품 데이터 (오류 발생)
</button>
</div>
<div>
<SectionErrorBoundary>
<UserProfile userId={userId} />
</SectionErrorBoundary>
<SectionErrorBoundary>
<ProductList products={products} />
</SectionErrorBoundary>
<SectionErrorBoundary>
<RecommendedProducts />
</SectionErrorBoundary>
</div>
</div>
</PageErrorBoundary>
);
}
에러 로깅과 모니터링
에러 정보 수집하기
class ProductionErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, eventId: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 에러 정보 객체 생성
const errorReport = {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
// 에러 로깅 (실제 서비스에서는 외부 서비스로 전송)
this.logError(errorReport);
}
logError = (errorReport) => {
// 로컬 스토리지에 저장 (임시)
const errors = JSON.parse(localStorage.getItem('errorLogs') || '[]');
errors.push(errorReport);
localStorage.setItem('errorLogs', JSON.stringify(errors));
// 실제로는 다음과 같은 서비스로 전송:
// - Sentry.captureException(error)
// - LogRocket.captureException(error)
// - 커스텀 API 엔드포인트로 전송
console.error('에러가 기록되었습니다:', errorReport);
};
render() {
if (this.state.hasError) {
return (
<div>
<h2>문제가 발생했습니다</h2>
<p>오류가 자동으로 기록되었습니다.</p>
<p>문제가 지속되면 고객 지원팀에 문의해 주세요.</p>
<button onClick={() => this.setState({ hasError: false })}>
다시 시도
</button>
</div>
);
}
return this.props.children;
}
}
// 에러 로그 확인 컴포넌트 (개발용)
function ErrorLogs() {
const [logs, setLogs] = useState([]);
const loadLogs = () => {
const errorLogs = JSON.parse(localStorage.getItem('errorLogs') || '[]');
setLogs(errorLogs);
};
const clearLogs = () => {
localStorage.removeItem('errorLogs');
setLogs([]);
};
return (
<div>
<h3>에러 로그</h3>
<button onClick={loadLogs}>로그 불러오기</button>
<button onClick={clearLogs}>로그 지우기</button>
{logs.map((log, index) => (
<details key={index}>
<summary>{log.timestamp} - {log.message}</summary>
<pre>{JSON.stringify(log, null, 2)}</pre>
</details>
))}
</div>
);
}
에러 바운더리 테스트
개발 중 에러 테스트
// 의도적으로 오류를 발생시키는 컴포넌트 (개발/테스트용)
function ErrorTrigger({ errorType }) {
if (errorType === 'render') {
throw new Error('렌더링 중 오류');
}
if (errorType === 'null') {
const obj = null;
return <div>{obj.property}</div>; // TypeError 발생
}
return (
<div>
<p>정상 컴포넌트</p>
<button onClick={() => {
if (errorType === 'event') {
throw new Error('이벤트 핸들러 오류');
}
}}>
이벤트 오류 테스트
</button>
</div>
);
}
function ErrorTestApp() {
const [errorType, setErrorType] = useState(null);
return (
<div>
<h2>에러 바운더리 테스트</h2>
<div>
<button onClick={() => setErrorType('render')}>
렌더링 오류 발생
</button>
<button onClick={() => setErrorType('null')}>
null 참조 오류 발생
</button>
<button onClick={() => setErrorType('event')}>
이벤트 오류 발생 (포착되지 않음)
</button>
<button onClick={() => setErrorType(null)}>
정상 상태
</button>
</div>
<ErrorBoundary>
<ErrorTrigger errorType={errorType} />
</ErrorBoundary>
</div>
);
}
정리
에러 바운더리의 핵심 개념을 학습했습니다:
- 에러 바운더리 필요성: React 애플리케이션 크래시 방지
- 클래스 컴포넌트 구현: getDerivedStateFromError와 componentDidCatch
- 에러 처리 전략: 페이지 레벨, 섹션 레벨의 다계층 에러 처리
- 한계점 이해: 이벤트 핸들러, 비동기 코드 오류는 포착하지 못함
- 에러 로깅: 실제 서비스에서의 에러 모니터링 방법
- 테스트: 개발 중 에러 바운더리 동작 확인
에러 바운더리는 사용자 경험을 크게 개선할 수 있는 중요한 기능입니다. 적절한 위치에 에러 바운더리를 배치하여 일부 컴포넌트의 오류가 전체 애플리케이션을 다운시키지 않도록 하는 것이 중요합니다.
다음 장에서는 성능 최적화 기법에 대해 더 자세히 알아보겠습니다.