Advanced

에러 핸들링 설계 패턴: Claude Code 실전 가이드

커스텀 에러 클래스, Result 타입, 글로벌 에러 핸들러, React Error Boundary까지 Claude Code로 타입 안전한 에러 처리 체계를 구축하는 방법.

Claude Code로 에러 핸들링 설계 개선하기

적절한 에러 핸들링은 애플리케이션의 견고함과 직결됩니다. Claude Code를 활용하면 타입 안전하고 일관된 에러 처리 패턴을 효율적으로 구현할 수 있습니다.

커스텀 에러 클래스 설계

> 앱 전체에서 사용할 커스텀 에러 클래스를 설계해줘.
> HTTP 상태 코드 매핑, 에러 코드, 사용자용 메시지까지 포함해서.
// errors/base.ts
export abstract class AppError extends Error {
  abstract readonly statusCode: number;
  abstract readonly code: string;
  readonly isOperational: boolean;

  constructor(message: string, isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }

  toJSON() {
    return {
      error: {
        code: this.code,
        message: this.message,
        ...(process.env.NODE_ENV === "development" && { stack: this.stack }),
      },
    };
  }
}

// errors/http.ts
export class NotFoundError extends AppError {
  readonly statusCode = 404;
  readonly code = "NOT_FOUND";

  constructor(resource: string, id?: string) {
    super(id ? `${resource}(${id})을(를) 찾을 수 없습니다` : `${resource}을(를) 찾을 수 없습니다`);
  }
}

export class ValidationError extends AppError {
  readonly statusCode = 400;
  readonly code = "VALIDATION_ERROR";
  readonly fields: Record<string, string[]>;

  constructor(fields: Record<string, string[]>) {
    super("유효성 검증 오류가 있습니다");
    this.fields = fields;
  }

  toJSON() {
    return {
      error: {
        code: this.code,
        message: this.message,
        fields: this.fields,
      },
    };
  }
}

export class UnauthorizedError extends AppError {
  readonly statusCode = 401;
  readonly code = "UNAUTHORIZED";
  constructor(message = "Authentication required") {
    super(message);
  }
}

export class ForbiddenError extends AppError {
  readonly statusCode = 403;
  readonly code = "FORBIDDEN";
  constructor(message = "권한이 없습니다") {
    super(message);
  }
}

Result 타입 패턴

예외를 쓰지 않고 타입 안전하게 에러를 다루는 Result 타입 패턴입니다.

> Result 타입 패턴을 구현해줘.
> 성공과 실패를 타입 레벨에서 구분할 수 있도록.
// types/result.ts
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

function ok<T>(data: T): Result<T, never> {
  return { success: true, data };
}

function err<E>(error: E): Result<never, E> {
  return { success: false, error };
}

// 사용 예시
async function findUser(id: string): Promise<Result<User, NotFoundError>> {
  const user = await db.user.findUnique({ where: { id } });
  if (!user) {
    return err(new NotFoundError("User", id));
  }
  return ok(user);
}

// 호출 측
const result = await findUser("123");
if (result.success) {
  console.log(result.data.name); // 타입 안전하게 접근
} else {
  console.log(result.error.message); // 에러 타입도 타입 안전
}

글로벌 에러 핸들러

Express용 글로벌 에러 핸들러 미들웨어를 구현해 봅시다.

> Express 글로벌 에러 핸들러 미들웨어를 만들어줘.
> AppError는 정해진 응답으로 변환, 예상치 못한 에러는 500으로.
> 프로덕션 환경에서는 스택 트레이스 숨김.
import { Request, Response, NextFunction } from "express";
import { AppError } from "../errors/base";

export function globalErrorHandler(
  err: Error,
  req: Request,
  res: Response,
  _next: NextFunction
) {
  // 로그 출력
  if (err instanceof AppError && err.isOperational) {
    console.warn(`[${err.code}] ${err.message}`);
  } else {
    console.error("Unexpected error:", err);
  }

  // AppError 인 경우 정의된 응답을 반환
  if (err instanceof AppError) {
    return res.status(err.statusCode).json(err.toJSON());
  }

  // 예상치 못한 에러
  res.status(500).json({
    error: {
      code: "INTERNAL_ERROR",
      message: process.env.NODE_ENV === "production"
        ? "서버 오류가 발생했습니다"
        : err.message,
    },
  });
}

비동기 에러 캐치

> Express async route handler의 에러를
> 자동으로 캐치하는 래퍼를 만들어줘.
import { Request, Response, NextFunction, RequestHandler } from "express";

function asyncHandler(
  fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
): RequestHandler {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
}

// 사용 예시: try-catch가 불필요
router.get("/users/:id", asyncHandler(async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) {
    throw new NotFoundError("User", req.params.id);
  }
  res.json({ data: user });
}));

프론트엔드 Error Boundary

> React Error Boundary를 만들어줘.
> 에러 종류에 따라 다른 UI를 표시하도록.
import { Component, ReactNode } from "react";

interface Props {
  children: ReactNode;
  fallback?: (error: Error, retry: () => void) => ReactNode;
}

interface State {
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error) {
    return { error };
  }

  handleRetry = () => {
    this.setState({ error: null });
  };

  render() {
    if (this.state.error) {
      if (this.props.fallback) {
        return this.props.fallback(this.state.error, this.handleRetry);
      }
      return (
        <div className="p-8 text-center">
          <h2 className="text-xl font-bold text-red-600">오류가 발생했습니다</h2>
          <p className="mt-2 text-gray-600">{this.state.error.message}</p>
          <button
            onClick={this.handleRetry}
            className="mt-4 rounded bg-blue-500 px-4 py-2 text-white"
          >
            다시 시도
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

디버깅과 에러 조사에 대한 구체적 방법은 디버깅 테크닉 완전 가이드, TypeScript에서의 타입 설계는 TypeScript 개발 활용법, 보안 관점의 에러 정보 처리는 보안 감사 자동화도 함께 참고해 보세요.

정리

일관된 에러 처리 설계는 애플리케이션의 견고함과 유지보수성을 크게 끌어올려 줍니다. Claude Code를 활용하면 커스텀 에러 클래스, Result 타입, 글로벌 핸들러까지 효율적으로 구현할 수 있습니다.

에러 핸들링 모범 사례는 Node.js 공식 가이드, Claude Code에 대해서는 Anthropic 공식 문서를 참고하세요.

#Claude Code #에러 핸들링 #디자인 패턴 #TypeScript #견고함

Claude Code 워크플로우를 한 단계 업그레이드하세요

지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.

무료 제공

무료 PDF: 5분 완성 Claude Code 치트시트

주요 명령어, 단축키, 프롬프트 예시를 A4 한 장에 정리했습니다.

PDF 다운로드
M

이 글을 작성한 사람

Masa

Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.