티스토리 뷰

소프트웨어 개발에서 "코드가 실행된다"는 것은 절반의 성공일 뿐입니다. 나머지 절반은 "예상치 못한 상황에서 코드가 어떻게 우아하게 실패하는가"에 달려 있습니다. 특히 네트워크 지연, 잘못된 사용자 입력, API 서버의 장애가 빈번한 현대 웹 생태계에서 에러 핸들링은 서비스의 신뢰도를 결정짓는 핵심 요소입니다.
단순히 에러를 잡는(catch) 것을 넘어, 시스템을 보호하고 사용자 경험을 해치지 않는 시니어 수준의 예외 처리 전략을 짚어보겠습니다.
1. Deep Dive: 왜 단순한 try...catch만으로는 부족할까?
대부분의 입문자는 모든 코드 블록을 try...catch로 감싸면 안전하다고 믿습니다. 하지만 이는 **"무차별적 수용"**이라는 함정에 빠지기 쉽습니다.
에러 핸들링의 본질: 전파(Propagation)와 억제(Suppression)
에러 핸들링의 핵심은 **"여기서 해결할 것인가, 아니면 상위 레이어로 보고할 것인가"**를 결정하는 것입니다. 모든 곳에서 에러를 잡아버리면(Silent Fail), 정작 디버깅이 필요한 시점에 에러의 근원지를 찾을 수 없는 '좀비 코드'가 생성됩니다.
비유로 이해하기 레스토랑에서 요리 도중 재료가 떨어졌다고 가정해 봅시다.
- 하위 레이어(요리사): 재료가 없음을 인지하고 매니저에게 알립니다. (Error Throwing)
- 상위 레이어(매니저): 손님에게 품절을 안내하거나 다른 메뉴를 추천합니다. (Error Handling)
만약 요리사가 재료가 없는데도 매니저에게 알리지 않고(Catch 후 아무것도 안 함) 빈 접시를 내보낸다면, 서비스 전체의 신뢰도가 무너지는 것과 같습니다.
2. Hands-on: 실무형 이커머스 결제 로직 예제
단순한 console.error가 아닌, 상태 복구와 로그 추적을 포함한 실전 결제 프로세스 코드를 살펴보겠습니다.
/**
* 이커머스 결제 처리 함수
* @param {string} orderId - 주문 ID
* @param {object} paymentInfo - 결제 수단 정보
*/
async function processOrderPayment(orderId, paymentInfo) {
try {
// 1. 재고 확인 (Pre-condition Check)
const stockAvailable = await checkInventory(orderId);
if (!stockAvailable) {
// 예상 가능한 비즈니스 에러는 커스텀 에러 객체를 사용합니다.
throw new Error("INSUFFICIENT_STOCK");
}
// 2. 결제 API 호출
const paymentResult = await externalPaymentGateway(paymentInfo);
// 3. 주문 상태 업데이트
await updateOrderStatus(orderId, 'PAID');
return { success: true, transactionId: paymentResult.id };
} catch (error) {
// [Troubleshooting] 에러의 성격에 따른 분기 처리
if (error.message === "INSUFFICIENT_STOCK") {
// 비즈니스 로직 에러: 사용자에게 친절한 안내 필요
console.warn(`[Order-Warn] 주문 ${orderId}: 재고 부족`);
return { success: false, reason: "재고가 부족하여 결제가 취소되었습니다." };
}
if (error.name === "TimeoutError") {
// 인프라 에러: 일시적인 문제이므로 재시도 권장
console.error(`[Order-Error] 결제 게이트웨이 타임아웃: ${error.stack}`);
return { success: false, reason: "결제 서버 응답이 지연되고 있습니다. 잠시 후 다시 시도해주세요." };
}
// 4. 예상치 못한 에러 (Critical)
// 로그 시스템(예: Sentry, Datadog)에 에러 전송
captureException(error, { extra: { orderId } });
// 치명적 에러 발생 시 시스템 안정성을 위해 상위로 에러를 다시 던집니다 (Re-throw)
throw new SystemError("결제 처리 중 내부 시스템 오류가 발생했습니다.", error);
} finally {
// 성공/실패 여부와 상관없이 실행 (예: 로딩 스피너 제거, 리소스 해제)
hideLoadingSpinner();
}
}
핵심 포인트 설명
- Custom Error Identification: error.message나 error.name을 통해 에러의 성격을 분류했습니다.
- Contextual Logging: 단순 에러 메시지가 아닌 orderId와 같은 컨텍스트를 함께 남겨 추적을 용이하게 했습니다.
- finally의 활용: 에러가 발생하더라도 UI의 상태(로딩 인디케이터 등)는 반드시 정리되어야 합니다.
3. Trade-offs: 에러 핸들링의 비용과 대안
완벽한 에러 핸들링에도 비용(Trade-off)이 따릅니다.
- 가독성 저하: 모든 라인을 try...catch로 감싸면 비즈니스 로직보다 에러 처리 코드가 더 길어지는 "Pyramid of Doom"이 발생할 수 있습니다.
- 대안: 미들웨어나 중앙 집중식 에러 핸들러(Global Error Boundary)를 활용하여 공통 로직을 분리하세요.
- 성능 오버헤드: 과도한 호출 스택 생성과 예외 객체 생성은 미세하게나마 메모리와 CPU를 소모합니다.
- 대안: 제어 흐름(Control Flow)을 위해 에러를 사용하지 마세요. (예: 단순히 데이터가 없는 경우 throw를 하기보다 null이나 Optional 패턴을 반환하는 것이 효율적입니다.)
4. 결론 및 요약
에러 핸들링은 단순히 앱이 죽지 않게 만드는 방어 기제가 아닙니다. 실패한 상황에서도 시스템이 다음 동작을 예측 가능하게 만드는 설계입니다.
- 비즈니스 에러와 시스템 에러를 구분하세요.
- 사용자에게는 이해 가능한 언어로, 개발자에게는 추적 가능한 로그로 응답하세요.
- try...catch는 해결할 수 있는 위치에서만 사용하고, 그렇지 않다면 과감히 위로 던지세요.
'Frontend > JAVASCRIPT' 카테고리의 다른 글
| Optional Chaining과 Nullish Coalescing: 런타임 에러를 방어하는 우아한 코드 설계 (0) | 2026.03.03 |
|---|---|
| 사용자 경험과 비용을 동시에 잡는 기술: 디바운스와 쓰로틀링 Deep Dive (0) | 2026.03.03 |
| 클래스 없는 상속의 마법, 자바스크립트 프로토타입 완벽 이해하기 (0) | 2026.03.02 |
| 브라우저의 기억 장치, LocalStorage 완벽 가이드: 데이터 지속성의 핵심 (0) | 2026.03.02 |
| 기술 블로그 작성사용자설정 Gem기술 블로그 작성님이 보낸 내용브라우저의 기억 장치, LocalStorage 완벽 가이드: 데이터 지속성의 핵심 (0) | 2026.03.02 |
- Total
- Today
- Yesterday
- LLM
- Nextjs
- 스마트안경
- 웹기초
- sLLM
- 멀티모달
- HBM
- on-device ai
- It용어
- CSR
- TypeScript
- Javascript
- 카카오
- 엣지컴퓨팅
- HTML
- SSR
- CSS
- 구글
- Rag
- MSA
- prompt engineering
- react
- AI
- 협력
- java
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |