에러 핸들링 이야기(2)

Hi·2023년 4월 16일
5

지난번에는 기본적인 에러 핸들링에 대해 알아봤습니다.

이번에는 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);
    }
  };

어떤 방식으로 에러가 잡히는지 간단하게 프로세스를 알아보면

  1. prepareServerSideProps에서 restApi 호출
  2. restApi에서 api 호출
  3. 오류가 발생한다면 interceptor나 ErrorHandler에서 에러 throw
  4. 전달된 오류는 prepareServerSideProps의 return에서 처리
  5. 상황에 맞게 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

profile
Hola!

0개의 댓글