Nest.js - Decorator 심화

맛없는콩두유·2025년 7월 2일
0
post-thumbnail

NestJS Decorator 심화 과정 정리

1. Swagger API Decorator

1.1 커스텀 Swagger Decorator 생성

src/common/decorator/swagger.decorator.ts에서 공통으로 사용할 수 있는 커스텀 Swagger Decorator를 생성했습니다.

import { applyDecorators, Type } from '@nestjs/common';
import { ApiCreatedResponse, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
import { PageResDto } from '../dto/res.dto';

export const ApiGetResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        allOf: [{ $ref: getSchemaPath(model) }],
      },
    }),
  );
};

export const ApiPostResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiCreatedResponse({
      schema: {
        allOf: [{ $ref: getSchemaPath(model) }],
      },
    }),
  );
};

export const ApiGetItemsResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        allOf: [
          { $ref: getSchemaPath(PageResDto) },
          {
            properties: {
              items: {
                type: 'array',
                items: { $ref: getSchemaPath(model) },
              },
            },
            required: ['items'],
          },
        ],
      },
    }),
  );
};

설명:

  • applyDecorators: 여러 데코레이터를 하나로 결합하는 유틸리티 함수
  • ApiGetResponse: GET 요청의 응답 스키마를 정의 (200 OK)
  • ApiPostResponse: POST 요청의 응답 스키마를 정의 (201 Created)
  • ApiGetItemsResponse: 페이지네이션이 포함된 배열 응답 스키마를 정의

1.2 Controller에서 사용 예시

@ApiTags('User')
@ApiExtraModels(FindUserReqDto, FindUserResDto, PageResDto)
@Controller('api/users')
export class UserController {
  @ApiBearerAuth()
  @ApiGetItemsResponse(FindUserResDto)
  @Get()
  findAll(@Query() { page, size }: PageReqDto, @User() user: UserAfterAuth) {
    return this.userService.findAll();
  }

  @ApiBearerAuth()
  @ApiGetResponse(FindUserResDto)
  @Get(':id')
  findOne(@Param() { id }: FindUserReqDto) {
    return this.userService.findOne(id);
  }
}

사용된 Decorator들:

  • @ApiTags('User'): Swagger UI에서 API 그룹을 'User'로 분류
  • @ApiExtraModels(): Body가 아닌 파라미터의 스키마를 Swagger에 추가
  • @ApiBearerAuth(): JWT 인증이 필요한 엔드포인트임을 표시 (잠금 표시)
  • @ApiGetResponse(), @ApiGetItemsResponse(): 커스텀 응답 스키마 적용

1.3 DTO에서 ApiProperty 사용 예시

export class SignupReqDto {
  @ApiProperty({ required: true, example: 'nestjs@fastcampus.com' })
  @IsEmail()
  @MaxLength(30)
  email: string;

  @ApiProperty({ required: true, example: 'Password1!' })
  @Matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{10,30}$/)
  password: string;
}

2. Validation & Transform

2.1 의존성 설치

package.json에서 필요한 패키지들이 설치되어 있습니다:

{
  "dependencies": {
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.0"
  }
}

2.2 main.ts에서 전역 설정

import { ValidationPipe } from '@nestjs/common';

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

  // ValidationPipe 전역 적용
  app.useGlobalPipes(
    new ValidationPipe({
      // class-transformer 적용
      transform: true,
    }),
  );
}

설명:

  • ValidationPipe: 요청 데이터의 유효성 검사를 자동으로 수행
  • transform: true: 요청 데이터를 DTO 클래스의 인스턴스로 자동 변환

2.3 DTO에서 Validation & Transform 사용 예시

export class PageReqDto {
  @ApiPropertyOptional({ description: '페이지. Default = 1' })
  @Transform(({ value }) => Number(value))
  @IsInt()
  page?: number = 1;

  @ApiPropertyOptional({ description: '페이지당 데이터 갯수. Default = 50' })
  @Transform(({ value }) => Number(value))
  @IsInt()
  size?: number = 50;
}

사용된 Decorator들:

  • @Transform(): 문자열로 전달된 쿼리 파라미터를 숫자로 변환
  • @IsInt(): 정수인지 검증
  • @ApiPropertyOptional(): 선택적 속성임을 Swagger에 표시

3. JWT 인증 시스템

3.1 JwtModule 등록

src/auth/auth.module.ts에서 JWT 모듈을 등록합니다:

@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.register({
      global: true,
      secret: 'temp secret',
      signOptions: { expiresIn: '1d' },
    }),
  ],
  providers: [
    AuthService,
    JwtStrategy,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AuthModule {}

설명:

  • JwtModule.register(): JWT 서비스 설정
  • global: true: 전역적으로 사용 가능
  • secret: JWT 토큰 서명에 사용할 비밀키
  • expiresIn: '1d': 토큰 만료 시간 (1일)
  • APP_GUARD: 전역 가드로 JwtAuthGuard 등록

3.2 JWT Strategy

src/auth/jwt.strategy.ts에서 JWT 토큰 검증 로직을 구현합니다:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'temp secret',
    });
  }

  // request.user
  async validate(payload: any) {
    return { id: payload.sub };
  }
}

설명:

  • ExtractJwt.fromAuthHeaderAsBearerToken(): Authorization 헤더에서 Bearer 토큰 추출
  • validate(): JWT 페이로드를 검증하고 사용자 정보 반환
  • payload.sub: JWT 토큰의 subject (보통 사용자 ID)

3.3 JWT Guard

src/auth/jwt-auth.guard.ts에서 인증 가드를 구현합니다:

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    return super.canActivate(context);
  }
}

설명:

  • AuthGuard('jwt'): Passport의 JWT 전략을 사용하는 가드
  • Reflector: 메타데이터를 읽기 위한 유틸리티
  • IS_PUBLIC_KEY: Public 데코레이터가 설정된 엔드포인트는 인증 없이 접근 가능

3.4 Controller에서 Guard 사용

@ApiTags('User')
@Controller('api/users')
export class UserController {
  @ApiBearerAuth()
  @ApiGetItemsResponse(FindUserResDto)
  @Get()
  findAll(@Query() { page, size }: PageReqDto, @User() user: UserAfterAuth) {
    return this.userService.findAll();
  }
}

설명:

  • @ApiBearerAuth(): Swagger UI에서 Bearer 토큰 인증이 필요함을 표시 (잠금 아이콘)
  • 전역 가드가 적용되어 있어 별도로 @UseGuards(JwtAuthGuard)를 명시하지 않아도 됨

3.5 main.ts에서 Bearer Auth 설정

const config = new DocumentBuilder()
  .setTitle('NestJS project')
  .setDescription('NestJS project API description')
  .setVersion('1.0')
  .addBearerAuth()
  .build();

설명:

  • addBearerAuth(): Swagger UI에서 Bearer 토큰 인증 기능 활성화
  • Swagger UI의 "Authorize" 버튼을 통해 JWT 토큰을 입력할 수 있음

4. Public Decorator

4.1 Public Decorator 구현

src/common/decorator/public.decorator.ts에서 Public 엔드포인트를 위한 데코레이터를 생성합니다:

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

설명:

  • SetMetadata: 메타데이터를 설정하는 유틸리티
  • IS_PUBLIC_KEY: 메타데이터 키로 사용할 상수
  • @Public(): 해당 엔드포인트가 인증 없이 접근 가능함을 표시

4.2 Public Decorator 사용 예시

@ApiTags('Auth')
@Controller('api/auth')
export class AuthController {
  @ApiPostResponse(SignupResDto)
  @Public()
  @Post('signup')
  async signup(@Body() { email, password, passwordConfirm }: SignupReqDto) {
    // 인증 없이 접근 가능
  }

  @ApiPostResponse(SigninResDto)
  @Public()
  @Post('signin')
  async signin(@Body() { email, password }: SigninReqDto) {
    // 인증 없이 접근 가능
  }
}

설명:

  • @Public(): 전역 JWT 가드가 적용되어 있어도 이 엔드포인트는 인증 없이 접근 가능
  • 로그인, 회원가입 등 인증이 필요 없는 엔드포인트에 사용

5. User Decorator

5.1 User Decorator 구현

src/common/decorator/user.decorator.ts에서 인증된 사용자 정보를 쉽게 추출할 수 있는 데코레이터를 생성합니다:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return request.user;
});

export interface UserAfterAuth {
  id: string;
}

설명:

  • createParamDecorator: 커스텀 파라미터 데코레이터 생성
  • request.user: JWT Strategy의 validate() 메서드에서 반환된 사용자 정보
  • UserAfterAuth: 인증 후 사용자 정보의 타입 정의

5.2 User Decorator 사용 예시

@Controller('api/users')
export class UserController {
  @Get()
  findAll(@Query() { page, size }: PageReqDto, @User() user: UserAfterAuth) {
    console.log(user); // { id: "user-id" }
    return this.userService.findAll();
  }
}

설명:

  • @User(): 인증된 사용자의 정보를 자동으로 추출
  • JWT 토큰에서 추출된 사용자 ID를 쉽게 사용 가능

6. Swagger UI 인증 유지

6.1 main.ts에서 persistAuthorization 설정

const customOptions: SwaggerCustomOptions = {
  swaggerOptions: {
    persistAuthorization: true,
  },
};
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, customOptions);

설명:

  • persistAuthorization: true: Swagger UI에서 페이지 새로고침 시에도 인증 정보 유지
  • 기본적으로는 새로고침 시 Authorize 버튼을 다시 눌러야 하지만, 이 설정으로 자동 유지됨

요약

이 프로젝트에서는 다음과 같은 Decorator 패턴들을 활용했습니다:

  1. Swagger Decorator: API 문서화를 위한 커스텀 데코레이터
  2. Validation Decorator: 데이터 유효성 검사 및 변환
  3. JWT 인증 시스템: 전역 가드와 Public 데코레이터를 통한 인증 제어
  4. User Decorator: 인증된 사용자 정보 추출
  5. Swagger UI 설정: 인증 정보 유지 및 Bearer 토큰 지원

이러한 패턴들을 통해 코드의 재사용성을 높이고, 일관된 API 문서화와 인증 시스템을 구축할 수 있습니다.

profile
하루하루 기록하기!

0개의 댓글