목록으로
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('결제에 실패했습니다. 다시 시도해주세요.');
}

마무리

좋은 에러 처리의 핵심은 다음과 같습니다.

  1. 에러를 무시하지 않습니다
  2. 사용자에게 적절한 피드백을 제공합니다
  3. 디버깅에 필요한 정보를 로깅합니다
  4. 복구 가능한 에러는 복구합니다

에러 처리 코드가 늘어나면 코드가 복잡해 보일 수 있습니다. 하지만 에러를 제대로 처리하지 않으면 프로덕션에서 더 큰 문제가 됩니다.