개발/library, framework

[NEST] custom validation pipe를 대체하는 간단한 로직

prpn97 2023. 9. 5. 04:01

발단

swagger를 사용하고 있는데, 컨트롤러 단에 swagger관련된 코드가 생기니 점점 가독성이 안좋아지는 것 같았다. 그래서 컨트롤러 단의 코드를 분산시켜주기 위해 유효성검증 관련 로직은 DTO에서 구현하고, body에 validationPipe를 사용하여 분산시켰다. 

그래서 입력값의 유효성 관련된 로직은 전부 DTO에서 처리하니, 컨트롤러 단에서 확실히 스크롤압박...은 많이 줄었는데 아쉬운 점이 있었다. 

DTO에서 처리하는 validatePipe가 한번에 걸러져서 에러가 전부 반환되는데 이 부분이 불편하다고 느껴졌다. 

말로는 어려운 것 같은데, 아래를 살펴보자. 

 

먼저 DTO에 입력한 내용이다. 

export class UpdateUserDto {
  @IsString()
  @Matches(
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
    {
      message: '비밀번호는 특수 문자를 포함한 8~20자여야 합니다.',
    },
  )
  @IsNotEmpty({ message: '비밀번호는 필수 항목입니다.' })
  readonly password: string;
  @IsString()
  @Matches(
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
    {
      message: '비밀번호 확인은 특수 문자를 포함한 8~20자여야 합니다.',
    },
  )
  @IsNotEmpty({ message: '비밀번호확인은 필수 항목입니다.' })
  readonly confirmPassword: string;

  @IsString()
  @Matches(
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
    {
      message: '새로운 비밀번호는 특수 문자를 포함한 8~20자여야 합니다.',
    },
  )
  @IsNotEmpty({ message: '새로운 비밀번호는 필수 항목입니다.' })
  readonly newPassword: string;
}

 

각 어노테이션별로 메세지를 별도로 기재했더니 message에 배열로 걸러져야 할 에러들이 한 번에 다 확인되었다. 이렇게 반환해야 할 때도 있겠지만, 어차피 유효성검증은 클라이언트측에서도 할테고, 서버에서도 처리하기는 하지만 당장 비밀번호를 입력하지 않아서 생기는 오류인데 다 나열할 필요는 없다고 느껴졌다. 그래서 여기에 에러를 한 개만 보여주고 하나씩 처리하도록 바꿔보려 한다. 

해결과정

방법 자체는 여러가지가 있겠다. 처음에는 validatePipe를 커스텀화해서 별도로 걸러주는 것을 생각했다. 그런데 아직 user에 대해서만 하고 있고, 먼저 간단하게 에러를 걸러서 보여주는 방법에 대해서 고민하고 구현해보고, 필요할 때 다른 방법에 대해서 구현하는게 정확하다고 생각했다.

 

그렇다면 어떻게 하면 에러를 걸러서 보여줄까 고민했는데, 생각해보니 결국 에러는 에러핸들러에 걸린다. 그러니까 에러처리하는 미들웨어에서 걸러주면 된다고 생각했다. 이 부분이 가드, 미들웨어 등 구조적으로 해치는 부분이 될지 아직 모르긴 하지만 당장 목적에 맞게 구현해보고, 차차 nest의 구조에 대해 정확히 파악하면서 다른 방법들도 살펴보려 한다. 

 

 

먼저 어떻게 해결했는지 살펴보자면,

1. 에러처리하는 미들웨어에 조건을 달아서 걸렀다. 

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = (exception as any).message.message;
    let detail = 'Unexpected Error';
    if (exception instanceof CommonError) {
      status = exception.status;
      message = exception.message;
      detail = exception.detail;

      return response.status(status).json({
        message: exception.detail,
        error: exception.message,
        statusCode: status,
      });
    } else if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.getResponse() as string;
      if (Array.isArray(message.message) && message.message.length > 1) {
        // 여러 에러 메시지 중 첫 번째 메시지 사용
        return response.status(status).json(message.message[0]);
      }
      return response.status(status).json(message);
    } else if (exception instanceof QueryFailedError) {
      status = HttpStatus.UNPROCESSABLE_ENTITY;
      message = (exception as QueryFailedError).driverError.code;
      return response.status(status).json(message);
    } else {
      console.error(exception);
      return response.status(status).json({
        error: detail,
        statusCode: status,
      });
    }
  }
}

 

중간에 exception.getResponse()로 message를 정해주는데, 만약 여기서 위 스샷처럼 에러메세지가 배열이라면 길이가 1을 초과(에러내용이 2개이상)인 경우 첫 번째 에러만 보여주도록 해서 바로 리턴했다. 

 

이제 에러가 1개만 보이는데, 문제는 비밀번호 확인은 필수 항목이라는 에러가 첫 번째로 보이면 좋겠는데, 다른 오류가 먼저 보였다. 입력하지 않았는데 특수문자나 글자수에 대한 오류가 나온다든지 하는 연관성이 약간 떨어지는 오류가 먼저 보일 수 있으니 순서를 정해주면 좋을 것 같다. 

 

2. DTO에서 에러처리 순서 정하기

export class UpdateUserDto {
  @IsString()
  @Matches(
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
    {
      message: '비밀번호는 특수 문자를 포함한 8~20자여야 합니다.',
    },
  )
  @IsNotEmpty({ message: '비밀번호는 필수 항목입니다.' })
  readonly password: string;
  }

중요한, 혹은 먼저 보여줄 내용일 수록 아래로 배치한다. 위의 경우, 회원정보를 수정할 때 비밀번호 입력하는 과정에서 아예 입력하지 않았을 때 필수 항목이라는 에러가 먼저 확인되어야 하고, 입력은 했는데 유효한지 여부를 확인하면서 생기는 에러가 다음으로 와야 한다. 

 

 

이렇게 하고 나면 원하는 에러를 바로 보여줄 수 있다. 

 

 

코멘트

Node.js 를 하면서 뭐든 직접 구현해왔다. jwt에 대해서 구현하려 하니 passport 라이브러리에 대해 가장 많이 나오는데, 이미 기존에도 직접 구현해왔던지라 오히려 nest에서 제공하는 기본 기능들이 편하면서도 한편으로는 어색할 때가 있다. 무조건적으로 사용하기보다는 가능한 간단한 부분들은 직접 구현해서 가볍게 가져가는 것도 괜찮은 방법이라고 생각한다. 

 

Nest 너무 재밌다! 진행하려고 하는 사이드프로젝트를 Nest로 하게 될 지는 아직 모르겠지만, 효율에 대해서 더욱 중시하며 코딩하는 방법에 대해 고민하고 싶다. 

728x90