본문 바로가기

NodeJS

풀스택 틱톡 클론 따라하기 - 5. 예외 처리 필터

반응형

https://youtu.be/Xy15S0nQxBQ

1:49:13 ~ 2:01:44

예외 처리 필터

// src/filters/custom-exception.filter.ts

import { ApolloError } from 'apollo-server-express';
import { ArgumentsHost, Injectable } from '@nestjs/common';
import { Catch } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
import { GqlExceptionFilter } from '@nestjs/graphql';

@Catch(BadRequestException)
export class GraphQLErrorFilter implements GqlExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentsHost) {
    // BadRequestException이 발생했을 때 실행되는 메서드

    const response = exception.getResponse();

    if (typeof response === 'object') {
      // 응답이 객체 형식인 경우 ApolloError를 활용하여 GraphQL 오류를 던집니다.
      throw new ApolloError('Validation error', 'VALIDATION_ERROR', response);
    } else {
      // 응답이 객체 형식이 아닌 경우 기본적인 ApolloError를 던집니다.
      throw new ApolloError('Bad Request');
    }
  }
}

 

이 필터는 BadRequestException을 캐치하고, 해당 예외에 대한 처리를 정의한다. GqlExceptionFilter를 구현하여 GraphQL 예외를 처리할 수 있도록 한다.

@Catch(BadRequestException): BadRequestException을 처리하겠다는 데코레이터이다.

catch 메서드 내부에서 예외를 처리하고, 응답이 객체 형식인 경우 ApolloError를 사용하여 유효성 검사 오류와 관련된 세부 정보를 전달한다.

만약 응답이 객체 형식이 아니라면 기본적인 ApolloError를 사용하여 'Bad Request'라는 간단한 오류 메시지를 전달한다.

 

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import { BadRequestException, ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // ...

  // 모든 요청에 대해 유효성 검사 수행.
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      exceptionFactory: (errors) => {
        // ...
      },
    }),
  );

  // ...
}

bootstrap();

 

main.ts 파일에서 app.useGlobalPipes는 글로벌 파이프를 설정하는 부분이다. 글로벌 파이프는 애플리케이션에서 모든 핸들러 및 라우터에 적용되는 설정을 의미한다. 여기에는 ValidationPipe가 설정되어 있다.

이때 ValidationPipe의 exceptionFactory 속성은 유효성 검사 오류가 발생했을 때 호출되는 사용자 정의 예외 팩토리 함수이다. 이 함수는 BadRequestException을 던진다.

이제 custom-exception.filter.ts 파일에서는 GqlExceptionFilter를 구현하여 GraphQL 예외를 처리한다. 이는 GraphQL의 컨텍스트에서 발생하는 예외에 대한 처리를 담당한다.

실제로 연결된 부분은 @nestjs/common 패키지의 @Catch 데코레이터를 사용하여 BadRequestException을 잡아서 GqlExceptionFilter를 통해 처리하도록 설정되어 있다. 이때, GraphQLErrorFilter 클래스에서는 해당 예외가 발생하면 Apollo 서버의 GraphQL 오류로 변환하여 반환한다.

이런식으로 예외 처리를 위한 다양한 전략들이 NestJS에서 사용된다. 만약 다른 예외에 대한 처리가 필요하다면, 추가적인 예외 필터를 작성하여 @Catch 데코레이터로 설정하고, 필요한 처리를 구현하면 된다.

 

// src/user/user.resolvers.ts

// ...
import { GraphQLErrorFilter } from 'src/filters/custom-exception.filter';

@Resolver()
export class UserResolver {
  // ...

  // @UseFilters(GraphQLErrorFilter): GraphQL 예외를 처리할 필터를 지정한다.
  // 이 데코레이터는 현재 뮤테이션에서 발생하는 예외를 처리하기 위해 GraphQLErrorFilter를 사용하겠다는 것을 나타낸다.
  @UseFilters(GraphQLErrorFilter)
  // 사용자 등록을 처리하는 뮤테이션
  @Mutation(() => RegisterResponse)
  async register(
    @Args('registerInput') registerDto: RegisterDto, // GraphQL 뮤테이션에서 전달된 입력 데이터
    @Context() context: { res: Response }, // Express 응답 객체
  ): Promise<RegisterResponse> {
 // ...
}

 

@UseFilters(GraphQLErrorFilter): 이 데코레이터는 현재 register 뮤테이션에서 발생하는 예외를 처리하기 위해 GraphQLErrorFilter를 사용하겠다는 것을 나타낸다.

이렇게 함으로써, register 뮤테이션에서 예외가 발생하면 해당 예외를 GraphQLErrorFilter에서 처리하게 된다. 만약 BadRequestException이 발생하면, 해당 예외에 대한 ApolloError를 던져 GraphQL 응답으로 전달한다.

필터 적용 전에는 비밀번호 8자 미만 입력하고 register 버튼 클릭 시 아래와 같이 에러 데이터를 받아온다.

 

[
  {
    "message": "Bad Request Exception",
    "locations": [
      {
        "line": 2,
        "column": 3
      }
    ],
    "path": ["register"],
    "extensions": {
      "code": "INTERNAL_SERVER_ERROR",
      "stacktrace": [
        "BadRequestException: Bad Request Exception",
        "    at ValidationPipe.exceptionFactory (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\src\\main.ts:57:15)",
        "    at ValidationPipe.transform (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\common\\pipes\\validation.pipe.js:74:30)",
        "    at processTicksAndRejections (node:internal/process/task_queues:95:5)",
        "    at resolveParamValue (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\helpers\\external-context-creator.js:136:31)",
        "    at async Promise.all (index 1)",
        "    at pipesFn (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\helpers\\external-context-creator.js:138:13)",
        "    at C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\helpers\\external-context-creator.js:66:17",
        "    at target (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\helpers\\external-context-creator.js:74:28)",
        "    at Object.register (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\helpers\\external-proxy.js:9:24)"
      ]
    }
  }
]

 

필터를 적용하면 아래와 같이 바뀐다.

 

[
  {
    "message": "Validation error",
    "locations": [
      {
        "line": 2,
        "column": 3
      }
    ],
    "path": ["register"],
    "extensions": {
      "password": "Password must be at least 8 characters.",
      "code": "VALIDATION_ERROR",
      "stacktrace": [
        "Error: Validation error",
        "    at GraphQLErrorFilter.catch (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\src\\filters\\custom-exception.filter.ts:19:13)",
        "    at ExternalExceptionsHandler.invokeCustomFilters (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\exceptions\\external-exceptions-handler.js:31:32)",
        "    at ExternalExceptionsHandler.next (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\exceptions\\external-exceptions-handler.js:14:29)",
        "    at Object.register (C:\\Users\\hwd30\\workspace\\tiktok_clone_fullstack\\tiktok_clone_backend\\node_modules\\@nestjs\\core\\helpers\\external-proxy.js:14:42)",
        "    at processTicksAndRejections (node:internal/process/task_queues:95:5)"
      ]
    }
  }
]

 

그럼 이제 클라이언트가 받는 에러 데이터가 이렇게 변경되어서, 프론트엔드의 Register.tsx에서

 

// ...

    // 서버에서 반환된 GraphQL 에러를 저장할 상태와 초기값을 설정한다.
    const [errors, setErrors] = useState<GraphQLErrorExtensions>({});

// ...

    }).catch((err) => {
      console.trace(err.graphQLErrors);

      // GraphQL 에러가 발생하면 해당 에러를 상태에 저장한다.
      setErrors(err.graphQLErrors[0].extensions);
    });

// ...

 

에러 데이터의 extensions 속성을 GraphQLErrorExtensions 타입의 errors에 담아, 이전에 Register.tsx에서 코드를 작성해두었던 대로 에러에 대한 처리를 진행할 수 있다.

이메일 중복 검사와 로그인이 아직 BadRequestException 처리되지 않아 수정이 필요하다.

 

// src/auth/auth.service.ts

// ...

  // 새로운 사용자 등록.
  async register(registerDto: RegisterDto, response: Response) {
    console.log('registerDto : ', registerDto);

    const existingUser = await this.prisma.user.findUnique({
      where: { email: registerDto.email },
    });

    if (existingUser) {
      throw new BadRequestException({ email: 'Email already in use.' }); // Provide a proper error response
    }

    const hashedPassword = await bcrypt.hash(registerDto.password, 10);

    const user = await this.prisma.user.create({
      data: {
        fullname: registerDto.fullname,
        password: hashedPassword,
        email: registerDto.email,
      },
    });

    console.log('user : ', user);

    return this.issueToken(user, response); // Issue tokens on registration
  }

  // 사용자의 로그인 처리.
  async login(loginDto: LoginDto, response: Response) {
    const user = await this.validateUser(loginDto);

    if (!user) {
      throw new BadRequestException({
        invalidCredentials: 'Invalid credentials.',
      }); // Provide a proper error response
    }

    return this.issueToken(user, response); // Issue tokens on login
  }

  // ...
}
// src/user/user.resolvers.ts

// ...

// @UseFilters(GraphQLErrorFilter): GraphQL 예외를 처리할 필터를 지정한다.
// 이 데코레이터는 현재 뮤테이션에서 발생하는 예외를 처리하기 위해 GraphQLErrorFilter를 사용하겠다는 것을 나타낸다.
@UseFilters(GraphQLErrorFilter)
@Resolver()
export class UserResolver {
  // ...

  // 사용자 등록을 처리하는 뮤테이션
  @Mutation(() => RegisterResponse)
  // ...
}

 

user.resolvers.ts에서 UseFilters 데코레이터를 뮤테이션 전체에 적용시키기 위해 클래스에 적용되게 하였다.

반응형