JavaScript
에러 처리 패턴과 모범 사례
견고한 애플리케이션을 만드는 에러 처리 전략. try-catch부터 전역 에러 핸들링까지 알아봅니다.
에러 처리는 안정적인 애플리케이션의 핵심입니다. 제대로 하지 않으면 사용자 경험이 나빠지고, 디버깅도 어려워집니다.
기본 에러 처리
try {
// 에러가 발생할 수 있는 코드
const data = JSON.parse(jsonString);
} catch (error) {
// 에러 처리
console.error('JSON 파싱 실패:', error.message);
} finally {
// 항상 실행 (선택적)
cleanup();
}
에러 다시 던지기
처리할 수 없는 에러는 다시 던집니다.
function processData(data) {
try {
return transform(data);
} catch (error) {
if (error instanceof ValidationError) {
// 처리 가능한 에러
return defaultValue;
}
// 처리 불가능한 에러는 다시 던짐
throw error;
}
}
커스텀 에러 클래스
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
this.code = code;
}
}
class ValidationError extends AppError {
constructor(message, field) {
super(message, 400, 'VALIDATION_ERROR');
this.name = 'ValidationError';
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource}을(를) 찾을 수 없습니다`, 404, 'NOT_FOUND');
this.name = 'NotFoundError';
}
}
// 사용
function getUser(id) {
const user = users.find(u => u.id === id);
if (!user) {
throw new NotFoundError('사용자');
}
return user;
}
비동기 에러 처리
Promise
// then/catch
fetchData()
.then(data => processData(data))
.catch(error => handleError(error));
// async/await
async function loadData() {
try {
const data = await fetchData();
return processData(data);
} catch (error) {
handleError(error);
return null;
}
}
여러 Promise 처리
// Promise.all - 하나라도 실패하면 전체 실패
try {
const [users, products] = await Promise.all([
fetchUsers(),
fetchProducts()
]);
} catch (error) {
// 어떤 요청이 실패했는지 알기 어려움
}
// Promise.allSettled - 각각의 결과 확인
const results = await Promise.allSettled([
fetchUsers(),
fetchProducts()
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log('성공:', result.value);
} else {
console.log('실패:', result.reason);
}
});
React 에러 처리
Error Boundary
컴포넌트 트리에서 에러를 잡아냅니다.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// 에러 리포팅 서비스로 전송
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// 사용
<ErrorBoundary>
<App />
</ErrorBoundary>
비동기 에러 처리 훅
function useAsync(asyncFunction) {
const [state, setState] = useState({
data: null,
loading: false,
error: null
});
const execute = useCallback(async (...args) => {
setState({ data: null, loading: true, error: null });
try {
const data = await asyncFunction(...args);
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState({ data: null, loading: false, error });
throw error;
}
}, [asyncFunction]);
return { ...state, execute };
}
// 사용
function UserProfile({ userId }) {
const { data: user, loading, error, execute } = useAsync(fetchUser);
useEffect(() => {
execute(userId);
}, [userId, execute]);
if (loading) return <Loading />;
if (error) return <Error message={error.message} />;
if (!user) return null;
return <div>{user.name}</div>;
}
전역 에러 핸들링
브라우저
// 동기 에러
window.onerror = (message, source, lineno, colno, error) => {
reportError(error);
return true; // 기본 에러 표시 방지
};
// Promise 에러
window.onunhandledrejection = (event) => {
reportError(event.reason);
event.preventDefault();
};
Node.js
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// 로그 기록 후 프로세스 종료
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
});
에러 로깅
function reportError(error, context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
name: error.name,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
...context
};
// 로깅 서비스로 전송
fetch('/api/logs/error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
}).catch(() => {
// 로깅 실패는 무시
});
}
에러 메시지 작성
// 나쁜 예
throw new Error('에러 발생');
throw new Error(error.toString());
// 좋은 예
throw new Error(`사용자 ${userId} 조회 실패: ${error.message}`);
throw new ValidationError('이메일 형식이 올바르지 않습니다', 'email');
사용자에게는 친절한 메시지를, 로그에는 상세한 정보를 남깁니다.
try {
await processPayment(orderId);
} catch (error) {
// 로그에는 상세 정보
console.error('결제 처리 실패:', { orderId, error });
// 사용자에게는 친절한 메시지
showToast('결제에 실패했습니다. 다시 시도해주세요.');
}
마무리
좋은 에러 처리의 핵심은 다음과 같습니다.
- 에러를 무시하지 않습니다
- 사용자에게 적절한 피드백을 제공합니다
- 디버깅에 필요한 정보를 로깅합니다
- 복구 가능한 에러는 복구합니다
에러 처리 코드가 늘어나면 코드가 복잡해 보일 수 있습니다. 하지만 에러를 제대로 처리하지 않으면 프로덕션에서 더 큰 문제가 됩니다.