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가 바로 인지하게 해주었다.
BadRequestError
클라이언트가 서버로 전송한 요청이 유효하지 않거나 서버가 이해할 수 없는 경우
UnauthorizedError
권한이 없는 api를 요청한 경우
ForbiddenError
요청된 리소스에 접근할 권한이 없는 경우
InternalServerError
server에서 발생한 에러
NotFoundError
존재하지 않는 api에 요청을 보낸 경우
ExceptionError
예상하지 못한 에러
먼저 Error들을 이렇게 크게 6개로 나누어주었다. 마지막 Exception Error에 대해서는 slack으로 메세지를 보내서 바로 확인할 수 있게 확인해주려한다.
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에서 처리할 수 없기 때문이다.
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에서 다룰 예정이다!
많은 도움이 되었습니다, 감사합니다.