Custom Error를 통한 front-end와의 소통

0

router.get(
	'/stores/:storeUUID', checkAccessToken,
	async (req: Request, res: Response, next: NextFunction) => {
		try {
			const { storeUUID } = req.params;

			if (!storeUUID) {
				throw new Error("Invalid query");
			}

			const storeData = await storeService.getStore(req.userUUID, storeUUID);

			const store = new EntireStoreDto(storeData);

			res.json({ data: store });
		} catch (error) {
			next(error);
		}
	}
);

기존에 우리서버의 에러 처리 방식은 Error객체로 해당 메세지만 front-end에게 보내주었다. 이렇게 문자열로 에러 메세지만 보내니, front-end에서는 해당 문자열을 일일히 해석하여 그에 맞는 처리를 해주어야 했다. 이 방식은 message 문자열을 실수로 잘못 입력한 경우에는 front측에서 에러 처리가 힘들다는 점이다. 또한 일관화 되지 않아서, 모든 에러에 대한 처리를 만들 수 도 없다.

따라서 custom-error를 도입하여 프론트에서 처리할 수 있는 에러에 대해서는 enum type으로 번호를 매겨주기로 하였다. 만약에 쿼리를 잘못보낸 경우에는 errorCode 2000기로 약속하고 front도 미리 2000번에 대한 대응을 해놓은 다음 에러가 났을때 front가 바로 인지하게 해주었다.

Error의 종류 나누기

  • BadRequestError
    클라이언트가 서버로 전송한 요청이 유효하지 않거나 서버가 이해할 수 없는 경우

  • UnauthorizedError
    권한이 없는 api를 요청한 경우

  • ForbiddenError
    요청된 리소스에 접근할 권한이 없는 경우

  • InternalServerError
    server에서 발생한 에러

  • NotFoundError
    존재하지 않는 api에 요청을 보낸 경우

  • ExceptionError
    예상하지 못한 에러

먼저 Error들을 이렇게 크게 6개로 나누어주었다. 마지막 Exception Error에 대해서는 slack으로 메세지를 보내서 바로 확인할 수 있게 확인해주려한다.

ErrorCode 정의

enum ErrorCode {
	// 인증 관련 에러 코드
	INVALID_LOGIN = 1000, // 잘못된 로그인 방식 -> 소셜 아이디로 로컬로그인 하는경우
	WRONG_PASSWORD = 1001, // 비밀번호 틀림
	EMAIL_SEND_EXCEED = 1002, // 이메일 전송 5회 초과
	WRONG_VERIFY_CODE = 1003, // 인증메일 코드가 일치하지 않음
	KAKAO_LOGIN_ERROR = 1004, // 카카오 로그인 에러

	// 토큰 관련 에러 코드
	INVALID_ACCESS_TOKEN = 1100, // 변조된 access
	EXPIRED_ACCESS_TOKEN = 1101, // access 토큰 시간 만료
	INVALID_REFRESH_TOKEN = 1102, // 변조된 refresh 토큰
	EXPIRED_REFRESH_TOKEN = 1103, // refresh 토큰 시간 만료
	NO_ACCESS_TOKEN_IN_HEADER = 1104, // access 토큰이 헤더에 없는 경우
	NO_REFRESH_TOKEN_IN_HEADER = 1105, // refresh 토큰이 헤더에 없는 경우

	// 유저 관련 에러 코드
	USER_NOT_FOUND = 1200, // 존재하지 않는 유저

	// 가게 관련 에러 코드
	STORE_NOT_FOUND = 1300, // 가게 조회시 없는 가게
	NO_STORE_UUID_QUERY = 1301, // api query에 가게 uuid가 없는 경우
	NO_STORE_REVIEW_UUID_IN_QUERY = 1302, // api query에 가게 리뷰 uuid가 없는 경우
	DUPLICATE_STORE_LIKE_ERROR = 1303, // 좋아요를 눌렀는데 다시 누르는 경우
	STORE_LIKE_NOT_FOUND = 1304, // 좋아요를 누른 기록이 없는데 좋아요를 취소하는 경우
	STORE_REVIEW_NOT_FOUND = 1305, // 가게 리뷰 조회시 없는 리뷰

	// 코스 관련 에러 코드
	COURSE_NOT_FOUND = 1400, // 코스 조회시 없는 코스
	DUPLICATE_COURSE_LIKE_ERROR = 1401, // 좋아요를 눌렀는데 다시 누르는 경우
	COURSE_LIKE_NOT_FOUND = 1402, // 좋아요를 누른 기록이 없는데 좋아요를 취소하는 경우
	COURSE_REVIEW_NOT_FOUND = 1403, // 코스 리뷰 조회시 없는 리뷰
	PRIVATE_COURSE = 1404, // 비공개인 코스를 가져오려 하는 경우

	// api 요청 에러
	INVALID_QUERY = 1500,

	// 기타 에러
	UNCATCHED_ERROR = 1600, // 예상 못한 에러
}

export default ErrorCode;

그 다음 errorCode들을 미리 enum type으로 정의해둔다. 이 에러들은 front에서 처리할 수 있는 것들에만 대해서 나누는게 좋다. 예를들어 서버에서 발생한 에러는 front에서 처리할 수 없기 때문이다.

Error 발생

if (!courseUUID) {
  throw new BadRequestError(ErrorCode.INVALID_QUERY, [
    { data: 'Invalid query' },
  ]);
}
.
.
.
next(error)

이제 만약 front에서 잘못된 query를 보냈다면 BadRequestError class로 mapping한 다음, next(error)로 다음 middleware로 넘겨준다.

const errorHandler = (
	err: any,
	req: Request,
	res: Response,
	next: NextFunction
): void => {
  if (err instanceof BadRequestError) {
    res
      .status(err.code)
      .send({ message: err.message, code: err.errorCode, errors: err.data });
  } else if (err instanceof UnauthorizedError) {
    res
      .status(err.code)
      .send({ message: err.message, code: err.errorCode, errors: err.data });
  } else if (err instanceof ForbiddenError) {
    res
      .status(err.code)
      .send({ message: err.message, code: err.errorCode, errors: err.data });
  } else if (err instanceof InternalServerError) {
    res
      .status(err.code)
      .send({ message: err.message, code: err.errorCode, errors: err.data });
  } else if (err instanceof NotFoundError) {
    res.status(err.code).send({ message: 'NOT_FOUND' });
  } else {
	// slack으로 message보내서 확인
  }
}

최종적으로 middleware에서 class별로 instanceof로 분기 후, Error종류별로 client에게 응답을 해주었다.
마지막 Exception Error에 대해서 어떻게 처리한지는 다음 post에서 다룰 예정이다!

profile
https://www.youtube.com/watch?v=__9qLP846JE

1개의 댓글

comment-user-thumbnail
2023년 7월 24일

많은 도움이 되었습니다, 감사합니다.

답글 달기