지난번에는 기본적인 에러 핸들링에 대해 알아봤습니다.
이번에는 nextjs
의 SSR(Server Side Rendering)환경에서 에러처리를 어떻게 하는지 알아볼 예정입니다.
nextjs
를 통해서 처음 프로젝트 세팅을 할 때 생소한 부분이 CSR(Client Side Rendering)과 SSR(Server Side Rendering)에서의 동작들을 구분하는 것이었습니다.
react
를 사용할때는 client에서 발생하는 에러들만을 처리하면 되었는데 SSR에서 발생하는 에러들도 고려를 해줘야 했습니다.
nextjs
에서 SSR을 사용하려면 getServerSideProps
라는 메서드를 사용해야 하는데 여기서 발생하는 에러들을 어떻게 처리하면 좋을지 고민해보았습니다.
// APIError.ts
import { ApiError } from 'next/dist/server/api-utils';
export class ServerSideError extends ApiError {
redirectUrl = '';
isNotFound = false;
constructor(statusCode: number, message: string) {
super(statusCode, message);
this.redirectUrl = '/unknown';
}
}
export class ServerError extends ServerSideError {
constructor(props?: string) {
super(500, props ?? '');
this.name = 'Server Error';
this.redirectUrl = '/500';
}
}
export class NotFoundError extends ServerSideError {
constructor(props?: string) {
super(404, props ?? '');
this.name = 'NotFoundError';
this.isNotFound = true;
}
}
export class UnavailableError extends ServerSideError {
constructor(props?: string) {
super(503, props ?? '');
this.name = 'Unavailable';
this.redirectUrl = '/503';
}
}
export class UnknownError extends ServerSideError {
constructor(props?: string) {
super(520, props ?? '');
this.name = 'Unknown';
this.redirectUrl = '/520';
}
}
export class ForbiddenError extends ServerSideError {
constructor(props?: string) {
super(403, props ?? '');
this.name = 'ForbiddenError';
this.redirectUrl = '/403';
}
}
export class AuthError extends ServerSideError {
constructor(props?: string) {
super(401, props ?? '');
this.name = 'AuthError';
this.redirectUrl = '/';
}
}
export const isInstanceOfAPIError = (object: unknown): object is ServerSideError =>
object instanceof ApiError && ('redirectUrl' in object || 'notFound' in object);
export const isAuthError = (error: unknown): error is AuthError =>
isInstanceOfAPIError(error) && error.name === 'AuthError';
자바스크립트의 기본 Error를 확장한 nextjs의 ApiError를 활용해서 우리가 기본적으로 마주하는 에러들에 대해서 만들어보았습니다.
getServerSideProps의 기본 함수
export async function getServerSideProps(context) {
const res = await fetch(`https://.../data`)
const data = await res.json()
if (!data) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}
getServerSideProps
에서 에러가 발생하거나 특정 상황이 발생하게 되면 redirect 객체를 리턴해줄 수 있는데 그런 점을 활용해서 에러가 발생한 페이지로 이동할 수 있도록 클래스를 만들었습니다.
그 다음에는 api 에러가 났을때 앞서 만들어준 클래스들을 활용할 수 있는 핸들러를 만들었습니다.
///ErrorHandler.ts
const ErrorHandler = ({ status, message = '' }: { status: number; message?: string }) => {
switch (status) {
case ERROR_STATUS_CODE.INTERNAL:
throw new ServerError(message);
case ERROR_STATUS_CODE.NOT_FOUND:
throw new NotFoundError(message);
case ERROR_STATUS_CODE.PERMISSION_DENIED:
throw new ForbiddenError(message);
case ERROR_STATUS_CODE.UNAUTHENTICATED:
throw new AuthError(message);
case ERROR_STATUS_CODE.UNAVAILABLE:
throw new UnavailableError(message);
default: {
if (status === ERROR_STATUS_CODE.UNAUTHENTICATED) {
throw new AuthError('');
}
throw new UnknownError('');
}
}
};
getServerSideProps
를 감싼 HoC방식의prepareServerSideProps
라는 이름의 함수를 만들어서 에러 처리를 해주었다. 이 함수는 SSR환경에서 유용하게 쓸 수 있는데 추후에 소개하고자 한다. 언젠가..
const prepareServerSideProps: PrepareServerSidePropsFunc =
({ getServerSidePropsFunc, accessibleRoles, }) =>
async (ctx) => {
const res = getServerSidePropsFunc();
if (res && (res.redirect || res.notFound)) {
if (res.notFound) {
return {
redirect: {
destination: res.notFound,
permanent: false,
},
};
}
return {
redirect: {
destination: res.redirect,
permanent: false,
},
};
}
여기까지 왔다면 SSR환경에서 에러 핸들링을 처리하기 위한 과정은 모두 끝났습니다!
이전 이야기에서 작성했던 코드를 다시 가지고 왔습니다.
//restApi.ts
export const restApi = async <Data = unknown>({
path,
requestBody,
axiosConfig = {},
queryClient,
}: ApiProps): Promise<Data> => {
const tokenFromCookie = //queryClient에서 토큰 가져오는 로직
const header = //헤더 설정
try{
const res = await axiosInstance({url: path});
return res.data;
}catch (e: unknown) {
if (tokenFromCookie && tokenUtils.isExpired(tokenFromCookie)) {
//refresh 로직
if (!newTokenRecord) {
// 로그아웃 로직
return ErrorHandler({ status: ERROR_STATUS_CODE.UNAUTHENTICATED });
}
//재요청
return restApi<Data>({
path,
requestBody,
axiosConfig: {
...axiosConfig,
headers: { ...header, ...tokenUtils.getAuthorizationObj(newTokenRecord) },
},
queryClient,
});
}
return interceptor(e);
}
};
어떤 방식으로 에러가 잡히는지 간단하게 프로세스를 알아보면
- prepareServerSideProps에서 restApi 호출
- restApi에서 api 호출
- 오류가 발생한다면 interceptor나 ErrorHandler에서 에러 throw
- 전달된 오류는 prepareServerSideProps의 return에서 처리
- 상황에 맞게 redirect 페이지로 가거나 not found 페이지로 이동
이제 SSR환경에서 에러 핸들링은 모두 마쳤습니다!
다음장에서는 react에서 에러 핸들링을 다뤄볼 예정입니다.
참조 : https://yceffort.kr/2021/10/api-error-handling-nextjs#1-%EC%97%90%EB%9F%AC-%EC%A0%95%EC%9D%98