Skip to content

idea: apply common error message#62

Draft
yabsed wants to merge 1 commit into
developfrom
refactor/common-error-msg
Draft

idea: apply common error message#62
yabsed wants to merge 1 commit into
developfrom
refactor/common-error-msg

Conversation

@yabsed
Copy link
Copy Markdown
Collaborator

@yabsed yabsed commented May 25, 2026

PR 제목

v2 API 에러 응답 공통 핸들러 도입

PR 본문

요약

v2 API에 한해 에러 응답을 공통 JSON envelope로 표준화했습니다.

기존에는 각 API route가 직접 try/catch를 두고 ZodError, BadRequestError, UserNotFoundError, unknown error 등을 개별 처리했습니다. 그래서 같은 status라도 route마다 body 형태와 메시지 대소문자가 달랐습니다.

이번 변경에서는 v2 route의 에러 처리 책임을 withV2ApiHandler로 옮겼습니다.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "요청 값이 올바르지 않습니다.",
    "details": [],
    "requestId": "..."
  }
}

핵심 원리

route handler는 이제 성공 흐름만 표현합니다.

예를 들어 clubs/register는 다음 일만 합니다.

  1. service 가져오기
  2. user 조회
  3. body validation
  4. service 호출
  5. 성공 응답 반환
const handler: NextApiHandler<ClubRegisterResponse> = async (req, res) => {
  const clubService = Provider.getService(ClubService)
  const userService = Provider.getService(UserService)

  const user = await userService.getUserByAccountId(req.headers.user as string)
  const body = ClubRegisterRequestSchema.parse(req.body)
  await clubService.registerClub(user.serviceUserId, body)

  return res.status(201).json({
    success: true,
    message: '동아리 등록 신청이 완료되었습니다.',
  })
}

짧아진 이유는 try/catch가 사라졌기 때문입니다. 사라진 게 아니라 아래 래퍼로 이동했습니다.

export default withV2ApiHandler({
  methods: ['POST'],
  handler,
  logPrefix: 'registerClub',
  mapError: mapRegisterError,
})

withV2ApiHandler가 하는 일

withV2ApiHandler는 v2 API route의 공통 입구입니다.

  1. 허용되지 않은 method면 405 METHOD_NOT_ALLOWED를 반환합니다.
  2. 실제 handler(req, res)를 실행합니다.
  3. handler나 service에서 throw된 에러를 catch합니다.
  4. route 전용 mapError가 있으면 먼저 적용합니다.
  5. 그 외 에러는 공통 매핑으로 변환합니다.
  6. 최종적으로 { error: { code, message, details, requestId } } 형태로 응답합니다.

공통 매핑 예시는 다음과 같습니다.

  • z.ZodError400 VALIDATION_ERROR
  • BadRequestError400 BAD_REQUEST 또는 구체 code
  • UnauthorizedError401 UNAUTHORIZED
  • ForbiddenError403 FORBIDDEN
  • UserNotFoundError → 기본 404 USER_NOT_FOUND
  • NotFoundError404 NOT_FOUND 또는 구체 code
  • ConflictError409 CONFLICT 또는 구체 code
  • unknown error → 500 INTERNAL_SERVER_ERROR

route별 예외는 mapError로 표현

clubs/register에서는 UserNotFoundError를 기본값인 404가 아니라 401로 내려야 합니다. 이런 route별 의미 차이는 mapError에만 남깁니다.

const mapRegisterError: ApiErrorMapper = (err) => {
  if (err instanceof UserNotFoundError) {
    return toApiError({
      status: 401,
      code: API_ERROR_CODES.UNAUTHORIZED,
      message: 'Unauthorized',
      cause: err,
    })
  }
}

즉, 대부분의 에러는 공통 규칙을 따르고, 정말 route 의미가 다른 경우만 local mapper로 덮어씁니다.

v2 전용 범위

이번 변경은 v2 API 표준화를 위한 작업입니다.

  • v2 route는 withV2ApiHandler를 사용합니다.
  • middleware도 /api/v2/ 요청에만 새 error envelope를 반환합니다.
  • v1 보호 API는 기존 plain text unauthorized 응답을 유지합니다.

적용된 대표 route

  • pages/api/v2/clubs/[uuid]/index.ts
  • pages/api/v2/clubs/search/index.ts
  • pages/api/v2/clubs/register/index.ts
  • pages/api/v2/managers/me/clubs/[uuid]/index.ts
  • pages/api/v2/terms/agree.ts
  • pages/api/v2/announcements/dismiss.ts
  • pages/api/v2/users/me/recent-searches/index.ts
  • pages/api/v2/users/me/voices/index.ts

검증

  • tsc --noEmit
  • eslint
  • prettier

@yabsed yabsed marked this pull request as draft May 25, 2026 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant