API 서버와의 통신 과정에서 발생할 수 있는 에러들과 이 에러들에 대한 적절한 처리를 통해 사용자 경험이 좋은 어플리케이션을 제공할 수 있습니다. 먼저 발생한 에러가 어떤 종류의 에러인지 파악하고 분류해야합니다. 에러가 파악되었다면 사용자가 처한 문맥에 맞게 적절한 에러처리를 진행하게 됩니다.
예제에서는 다음의 라이브러리들과 API에 대한 선수지식이 필요합니다.
- Axios
- TanStack Query
- Error Boundary
에러 정의하기
에러가 어떻게 발생할 수 있는지를 중심으로 그 종류에 대해서 정리하겠습니다. 에러의 출처를 기준으로 에러의 종류를 나눠보겠습니다.
사용자 에러
에러의 원인이 애플리케이션 사용자에게 있는 에러입니다. HTTP Status Code 중 아래의 경우로 볼 수 있습니다.
- 400: Bad Request - 잘못된 요청
- 401: Unauthorized - 인증되지 않은 사용자
- 403: Forbidden - 사용자 권한부족, 차단된 IP주소
클라이언트 에러
에러의 원인이 클라이언트 측에 있는 에러입니다. 개발단계에서 개발자의 실수로 발생할 확률이 높습니다. HTTP Status Code 중 아래의 경우로 볼 수 있습니다.
- 404: Not Found - 존재하지 않는 URL 요청
- 405: Method Not Allowed - 해당 URL로 허용되지 않은 메소드로 요청했을 때 (GET만 있는데 POST를 했다거나)
- 그 외의 400번대 에러들
서버 에러
에러의 원인이 서버에게 있는 에러입니다. HTTP Status Code 중 아래의 경우로 볼 수 있습니다.
- 500: Internal Server Error
- 502: Bad Gateway
- 503: Service Unavailable
- 504: Gateway Timeout
- 그 외 더 많은 500번대 에러들
예상치 못한 에러
서버가 응답하지 않거나 네트워크 연결이 끊어진 경우 axios
에서 다음의 에러를 전달합니다.
- ECONNABORTED: 요청의 타임아웃
- ENOTFOUND: 서버를 찾을 수 없는 경우
그 외 요청 과정에서 발생하는 예상할 수 없는 에러들이 있습니다.
에러 분류하기
앞에서 개념적으로 에러 종류에 대해서 정의해보았습니다. 이제 본격적인 에러 처리에 앞서 런타임에서 발생한 에러를 분류하는 과정을 거쳐야합니다.
에러 클래스 정의하기
서버에 서 발생하는 에러정보를 분류할 수 있도록 HttpError
클래스를 정의합니다.
export const enum HttpErrorType {
USER = 'UserError',
AUTH = 'AuthError',
FORBIDDEN = 'ForbiddenError',
CLIENT = 'ClinetError',
SERVER = 'ServerError',
UNEXPECTED = 'UnexpectedError',
}
export type HttpErrorInfo<E = unknown> = {
type: HttpErrorType
errorCode: string | number
statusCode?: number
detail?: E
meta?: unknown
}
export class HttpError<E = unknown> extends Error {
private _type: HttpErrorType
private _errorCode: string | number
private _statusCode?: number
private _detail?: E
private _meta
constructor({ type, errorCode, statusCode, detail, meta }: HttpErrorInfo<E>) {
const generalMessage = `${type}: [${errorCode}]`
super(generalMessage)
this._type = type
this._errorCode = errorCode
this._statusCode = statusCode
this._detail = detail
this._meta = meta
}
get type() {
return this._type
}
get errorCode() {
return this._errorCode
}
get statusCode() {
return this._statusCode
}
get detail() {
return this._detail
}
get meta() {
return this._meta
}
printAll(...args: unknown[]) {
console.error(...args, this.message, {
type: this._type,
errorCode: this._errorCode,
statusCode: this._statusCode,
detail: this._detail,
meta: this._meta,
})
}
}
에러 클래스를 생성하기 쉽고, 변화에 유연하게 대응할 수 있도록 팩토리 함수를 만듭니다.
// 상태코드로 에러를 종류를 분류합니다.
export function getErrorTypeFromStatusCode(statusCode?: number) {
if (!statusCode) {
return HttpErrorType.UNEXPECTED
}
if (statusCode < 500) {
switch (statusCode) {
case 400:
return HttpErrorType.USER
case 401:
return HttpErrorType.AUTH
case 403:
return HttpErrorType.FORBIDDEN
default:
return HttpErrorType.CLIENT
}
}
return HttpErrorType.SERVER
}
// 에러 클래스를 생성해주는 팩토리 함수입니다.
export function getHttpError<D = unknown>(
errorCode: string | number,
detail?: D,
statusCode?: number,
meta?: unknown
) {
return new HttpError({
type: getErrorTypeFromStatusCode(statusCode),
errorCode: errorCode,
detail,
statusCode,
meta,
})
}
// HttpError인지 체크하는 타입가드입니다.
export function isHttpError<E = unknown>(
error: unknown
): error is HttpError<E> {
return error instanceof HttpError
}
런타임 에러 분류
Axios를 통해서 HTTP 통신을 하고 있기 때문에, 적절하게 에러를 분류할 수 있는 곳은 interceptor
로 판단됩니다. 응답이 발생한 후 즉각적으로 처리할 수 있기 때문입니다. 위에서 정의한 HttpError
를 사용해 아래와 같이 응답 인터셉터 안에서 분류하겠습니다.
const instance = axios.create({
baseURL: /* base url */,
})
instance.interceptors.response.use(undefined, (error) => {
if (isAxiosError<ErrorResponseData>(error)) {
const statusCode = error.response?.status
const errorDetail = error.response?.data
// 서버에서 별도의 에러코드를 내려줄 것으로 예상합니다.
const errorCode = errorDetail?.code || 'UNKNOWN_ERROR'
// 서버로 받은 응답을 토대로 HttpError를 생성하여 반환합니다.
return Promise.reject(getHttpError(errorCode, errorDetail, statusCode))
}
return Promise.reject(error)
})
이후 에러가 catch
되는 부분들에서 HttpError
의 정보를 가지고 있을 수 있게 됩니다. interceptor
안에서 여러 인증에러와 같은 공통 에러 처리를 진행할 수도 있으나, 에러 처리에 대한 부분은 요청을 발생한 문맥에서 적절하게 판단하여 처리하는 것이 맞다고 생각합니다. 이에 따라 여기에서는 에러에 대한 분류까지만 진행합니다.
에러 처리 분류하기
위에서 정의한 에러에 따라 에러 처리 방법을 아래와 같이 정리하였습니다.
사용자 원인
에러의 원인이 사용자에게 있으므로, 적절한 피드백을 통해서 사용자가 성공한 응답을 받을 수 있도록 유도하여야 합니다.
- 토스트 메시지