오류 처리는 애플리케이션이 예기치 않은 상황을 우아하게 처리할 수 있도록 보장하는 프로그래밍의 기본적인 측면입니다. JavaScript에서는 try-catch
가 일반적으로 사용되지만, 오류 처리를 향상시키기 위한 더 고급 기술이 있습니다.
이 글에서는 이러한 고급 방법을 탐구하고, 오류 관리 전략을 개선하고 애플리케이션을 더 견고하게 만들기 위한 실용적인 솔루션을 제공합니다.
오류 처리란 무엇인가?
오류 처리의 목적
오류 처리는 프로그램 실행 중 발생하는 문제를 예측하고, 감지하며, 대응하는 것입니다. 적절한 오류 처리는 사용자 경험을 개선하고, 애플리케이션의 안정성을 유지하며, 신뢰성을 보장합니다.
JavaScript의 오류 유형
- 구문 오류. 이는 괄호 누락이나 키워드의 잘못된 사용과 같은 코드 구문에서의 실수입니다.
- 런타임 오류. 실행 중 발생하며, 정의되지 않은 객체의 속성에 접근하는 것과 같은 경우입니다.
- 논리 오류. 이러한 오류는 프로그램이 중단되지 않지만 잘못된 결과를 초래하며, 종종 결함 있는 논리나 의도치 않은 부작용으로 인해 발생합니다.
왜 try-catch만으로는 충분하지 않은가?
try-catch의 한계
- 범위 제한. 동기 코드 블록 내에서만 처리하며, 특별히 처리되지 않는 한 비동기 작업에는 영향을 미치지 않습니다.
- 은음의 실패. 과용 또는 부적절한 사용은 오류가 은음으로 무시되어 예기치 않은 동작을 일으킬 수 있습니다.
- 오류 전파. 애플리케이션의 다른 계층을 통해 오류를 전파하는 것을 기본적으로 지원하지 않습니다.
try-catch 사용 시기
- 동기 코드. JSON 파싱과 같은 동기 작업에서 오류를 처리하는 데 효과적입니다.
- 임계 구간. 오류가 심각한 결과를 초래할 수 있는 중요한 코드 구간을 보호하는 데 사용합니다.
사용자 정의 오류 클래스: 오류 정보 개선
사용자 정의 오류 클래스 생성
사용자 정의 오류 클래스는 내장 Error
클래스를 확장하여 추가 정보를 제공합니다:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.stack = (new Error()).stack; // Capture the stack trace
}
}
사용자 정의 오류의 장점
- 명확성. 구체적인 오류 메시지를 제공합니다.
- 세분화된 처리. 특정 오류 유형을 별도로 처리할 수 있습니다.
- 오류 메타데이터. 오류에 대한 추가 문맥을 포함합니다.
사용 사례별 사용자 정의 오류
- 유효성 검사 실패. 사용자 입력 유효성 검사와 관련된 오류입니다.
- 도메인별 오류. 인증 또는 결제 처리와 같은 특정 애플리케이션 도메인에 맞춘 오류입니다.
중앙화된 오류 처리
Node.js의 글로벌 오류 처리
미들웨어를 사용하여 오류 처리를 중앙화합니다:
app.use((err, req, res, next) => {
console.error('Global error handler:', err);
res.status(500).json({ message: 'An error occurred' });
});
프론트엔드 응용 프로그램의 중앙화된 오류 처리
React를 사용하여 중앙 집중식 오류 처리를 구현하세요:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
중앙 집중식 오류 처리의 장점
- 일관성. 오류 관리에 대한 균일한 접근 방식을 보장합니다.
- 보다 쉬운 유지 보수. 중앙 집중식 업데이트는 변경 사항을 놓치는 위험을 줄입니다.
- 더 나은 로깅 및 모니터링. 모니터링 도구와의 통합을 용이하게 합니다.
오류 전파
동기 코드에서의 오류 전파
오류를 전파하기 위해 throw
를 사용하세요:
function processData(data) {
try {
validateData(data);
saveData(data);
} catch (error) {
console.error('Failed to process data:', error);
throw error;
}
}
비동기 코드에서의 오류 처리
프로미스나 async
/await
로 오류를 처리하세요:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
오류를 전파해야 하는 시점
- 중대한 오류. 전체 애플리케이션에 영향을 미치는 오류를 전파하세요.
- 비즈니스 로직. 상위 수준 컴포넌트가 비즈니스 로직 오류를 처리하도록 허용하세요.
비동기 코드에서의 오류 처리
async/await로 오류 처리하기
비동기 함수에서 오류를 관리하기 위해 try-catch
를 사용하세요:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
return null;
}
}
Promise.all과 함께 오류 처리하기
여러 프로미스와 오류를 처리하세요:
async function fetchMultipleData(urls) {
try {
const responses = await Promise.all(urls.map
(url => fetch(url)));
return await Promise.all(responses.map(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${response.url}`);
}
return response.json();
}));
} catch (error) {
console.error('Error fetching multiple data:', error);
return [];
}
}
비동기 오류 처리에서 흔한 함정
- 잡히지 않는 프로미스. 항상
await
,.then()
, 또는.catch()
를 사용하여 프로미스를 처리하세요. - 무음 실패. 오류가 음성으로 처리되지 않도록 보장하세요.
- 레이스 조건. 동시 비동기 작업에 주의하십시오.
에러 로깅
클라이언트 측 에러 로깅
전역 오류 캡처:
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error captured:', message, source, lineno, colno, error);
sendErrorToService({ message, source, lineno, colno, error });
};
서버 측 에러 로깅
Winston과 같은 도구를 사용하여 서버 측 로깅:
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'error.log' })]
});
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).send('An error occurred');
});
모니터링 및 경보
PagerDuty 또는 Slack과 같은 서비스로 실시간 모니터링 및 경보 설정:
function notifyError(error) {
// Send error details to monitoring service
}
에러 로깅을 위한 모범 사례
- 컨텍스트 포함. 요청 데이터 및 사용자 정보와 같은 추가 컨텍스트를 기록하십시오.
- 과도한 로깅 피하기. 성능 문제를 방지하기 위해 필수 정보만을 기록하십시오.
- 로그 정기적 분석. 반복되는 문제를 감지하고 해결하기 위해 정기적으로 로그를 검토하십시오.
우아한 저하 및 대체 기능
우아한 저하
기능이 감소된 상태로 계속 작동하도록 응용프로그램을 설계하십시오:
function renderProfilePicture(user) {
try {
if (!user.profilePicture) {
throw new Error('Profile picture not available');
}
return `<img data-fr-src="${user.profilePicture}" alt="Profile Picture">`;
} catch (error) {
console.error('Error rendering profile picture:', error.message);
return '<img src="/default-profile.png" alt="Default Profile Picture">';
}
}
대체 메커니즘
기본 작업이 실패할 때 대체 수단을 제공하십시오:
async function fetchDataWithFallback(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
return { message: 'Default data' }; // Fallback data
}
}
우아한 저하 구현
- UI 대체. 기능이 실패할 때 대체 UI 요소를 제공하십시오.
- 데이터 대체. 실시간 데이터를 사용할 수 없을 때 캐시된 값이나 기본값을 사용하십시오.
- 재시도 메커니즘. 일시적 오류에 대한 재시도 논리를 구현하십시오.
우아한 저하 균형 맞추기
사용자에게 문제에 대한 정보를 제공하면서 대처 방안을 유지하는 것의 균형:
function showErrorNotification(message) {
// Notify users about the issue
}
오류 처리 테스트
단위 테스트 오류 처리
개별 함수에서 오류 처리 확인:
const { validateUserInput } = require('./validation');
test('throws error for invalid username', () => {
expect(() => {
validateUserInput({ username: 'ab' });
}).toThrow('Username must be at least 3 characters long.');
});
통합 테스트
다양한 응용 프로그램 레이어에서 오류 처리 테스트:
test('fetches data with fallback on error', async () => {
fetch.mockReject(new Error('Network error'));
const data = await fetchDataWithFallback('https://api.example.com/data');
expect(data).toEqual({ message: 'Default data' });
});
끝-끝 테스트
오류 처리를 테스트하려면 실제 시나리오를 모방:
describe('ErrorBoundary', () => {
it('displays error message on error', () => {
cy.mount(<ErrorBoundary><MyComponent /></ErrorBoundary>);
cy.get(MyComponent).then(component => {
component.simulateError(new Error('Test error'));
});
cy.contains('Something went wrong.').should('be.visible');
});
});
오류 처리 테스트의 모범 사례
- 경계 케이스 다루기. 테스트가 다양한 오류 시나리오를 다루도록 보장하십시오.
- 대비책 테스트. 대비책 메커니즘이 의도대로 작동하는지 확인하십시오.
- 테스트 자동화. CI/CD 파이프라인을 사용하여 자동화하고 견고한 오류 처리를 보장하십시오.
실제 시나리오
시나리오 1: 결제 처리 시스템
결제 처리 중 오류 처리:
- 사용자 정의 오류 클래스.
CardValidationError
,PaymentGatewayError
와 같은 클래스 사용. - 재시도 논리. 네트워크 관련 문제에 대한 재시도 구현.
- 중앙 집중식 로깅. 결제 오류를 모니터링하고 즉시 문제 해결.
시나리오 2: 데이터 집약적 응용 프로그램
데이터 처리 오류 관리:
- 우아한 저하. 부분 데이터 또는 대체 뷰 제공.
- 대체 데이터. 캐시된 또는 기본값 사용.
- 오류 로깅. 문제 해결을 위한 상세한 컨텍스트 기록.
시나리오 3: 사용자 인증 및 권한
인증 및 권한 오류 처리:
- 사용자 정의 오류 클래스.
AuthenticationError
,AuthorizationError
와 같은 클래스 생성. - 중앙 처리. 인증 관련 문제를 기록하고 모니터링.
- 우아한 저하. 대체 로그인 옵션 및 의미 있는 오류 메시지 제공.
결론
JavaScript에서 고급 오류 처리는 단순한 try-catch
를 넘어서 사용자 정의 오류, 중앙 처리, 전파 및 견고한 테스트를 포함해야 합니다. 이러한 기술을 구현하면 문제가 발생할 때도 원활한 사용자 경험을 제공하는 강건한 애플리케이션을 구축할 수 있습니다.
추가 자료
- 다그라스 크록포드(Douglas Crockford)의 “자바스크립트: 좋은 파트”
- 카일 심슨(Kyle Simpson)의 “You Don’t Know JS: Async & Performance”
- MDN 웹 문서: 오류 처리
Source:
https://dzone.com/articles/advanced-error-handling-in-javascript-custom-error