끄적끄적

[NestJS] nestia 를 활용해서 가독성 있는 코드 만들기 본문

개발/js & ts & node.js

[NestJS] nestia 를 활용해서 가독성 있는 코드 만들기

코리이 2024. 3. 3. 18:18

최근에 필자가 속해있는 회사에서 nestia 를 사용하도록 서버 리펙토링을 완료했다. 참고로 nestia 란 라이브러리는 Nestjs 를 조금 더 쉽게 쓸 수 있도록 해주며 성능적으로도 훨씬 빠르게 만들어 주는 라이브러리로, 한국에 있는 개발자 분이 개발한 멋진 라이브러리다. 이전 포스팅에서 nestia sdk 에 대해서 간단하게는 남겼지만 모노레포 지원이 잘 안되는 이슈로 이는 적용 못했지만 굳이 sdk 를 쓰지 않더라도 가독성 및 생산성 면에서 훨씬 좋아지는 경험을 했기에 이번에 포스팅을 남겨보고자 한다.

DTO 를 Interface 로 변경

기존에 class-transformer, class-validator 를 활용했을 때의 코드를 우선 확인해보자. 아래 코드가 왜 나오는지 이해하기 위해서는 nodejs 의 typescript 의 경우 javascript 로 컴파일 후 이 빌드된 js 파일을 활용한다는 것을 알아야 한다. 여기서 js 의 경우 "동적타입언어" 이기 때문에 { name: 'pawmi' } 라는 json 이 들어오기 전에 name 이 string 인지, number 인지 bool 인지 등 "미리 파악 할 수 없다" 는 문제가 존재한다. 따라서 이를 위해 class-validator 에서는 @IsString() 라는 데코레이터를 활용해서 "이 프로퍼티가 string 이야" 라는 표식을 해주고 이 표식을 가져와서 class-validator 가 검증해주는 구조로 작동한다.

또한 대부분의 스타트업 백엔드 개발자라면 API 문서로 swagger 라는 문서를 활용할 것이라 생각한다. 이를 직접적으로 다 만들어주는 것은 어려우니 다른 대부분의 언어에서도 그렇듯이 데코레이터(python)나 어노테이션(java), 혹은 주석(go, express 등)을 활용해서 자동으로 swagger 문서를 생성하도록 설정해주는 라이브러리를 활용하게 된다. 

 

이 때문에 아래와 같이 데코레이터가 덕지덕지 달린 DTO 가 만들어지게 된다.

export class ChangeChatRoomSettingCommand implements ICommand {
    @IsString()
    @IsUUID('4')
    @ApiProperty()
    readonly roomId: string;

    @IsString()
    @ApiProperty()
    readonly title: string;

    @IsString()
    @IsOptional()
    @ApiProperty({ type: 'string', nullable: true })
    readonly description?: string | null;

    @IsBoolean()
    @ApiProperty()
    readonly onAirDrop: boolean;

    @IsBoolean()
    @ApiProperty()
    readonly isPriceOpened: boolean;

    @IsArray()
    @ArrayMaxSize(10)
    @Length(1, 29, { each: true)
    @ApiProperty({ type: [String] })
    readonly tags: string[];
    
    // ...
}

 

정말 복잡하고 가독성도 떨어진다. 또한 dto 는 정말 "DTO" 로 수정이 최대한 안이루어져야 하므로 개인적으로는 "interface" 로 정의되는게 맞다고 보는데 이는 라이브러리 특성상 어쩔 수 없이 class 로 강제되면서 typescript 를 제대로 활용할 수 없도록 하는 단점이 존재한다. 그럼 이제 이를 nestia (typia) 로 수정하면 아래처럼 깔끔하게 인터페이스만 정의한 코드를 만들 수 있다. nestia 에서는 swagger 를 데코레이터가 아니라 ts comilier api 를 활용해 명령어를 통해 swagger.json 파일을 제너레이팅 할 수 있도록 도와준다.

export interface Command extends ICommand {
    readonly roomId: string & tags.Format<'uuid'>;
    
    readonly title: string & tags.MaxLength<30>;

    readonly description?: (string & tags.MaxLength<200>) | null;

    readonly onAirDrop: boolean;

    readonly isPriceOpened: boolean;

    readonly tags: Array<string & tags.MaxLength<30>> & tags.MaxItems<10>;
}

 

아래는 두 코드를 비교한 스크린샷이다. (코드와는 조금 다를 수 있지만 그냥 보기에도 확연히 가독성이 뛰어남을 확인할 수 있다. 또한 class-validator 를 사용할 때는 쓰기 어려웠던 union 타입 등도 적용이 가능해서 조금 더 typescript 스럽게 쓸 수 있게 되었다.

Api Router 에 존재하는 swagger decorator 삭제

nestjs 를 사용할때 기본 response 를 사용하는 사람은 드물 것으로 생각된다. 특히 에러코드를 커스텀하거나 페이지네이션의 동일 스트럭처를 미리 정해놓고 리턴하는 방식을 쓰지 않을까 싶다. 하지만 nestjs swagger 에서 이를 활용하려면 기본적인 방식으로는 해결하기 어렵다. 방금 든 예시 뿐만 아니라 여러가지 이유로 인해서 결국 nestjs swagger 를 활용한다면 어떤 방식으로든 swagger 데코레이터를 커스텀하게 될 것이라 생각한다. 필자의 회사에서는 초기에 아래처럼 커스텀 모델의 정의해서 스키마에 추가할 수 있도록 설정해서 사용중에 있었다.

@Put('rooms/:roomId')
@UseGuards(JwtGuard)
@HttpCode(200)
// 아래부터 swagger 코드
@ApiOperation({description: "채팅방 정보를 변경한다."})
@ApiExtraModels(ChatRoomResponse)
@ApiSwaggerResponse(ChatRoomResponse)
@ApiParam({name: 'roomId'})
async postChangeRoom(/* params */) {
    /* logics */
}

 

위에서 만약 api query 가 함께 더 많은 것들이 추가된다면 @ApiQuery 와 같은 데코레이터가 더 많이 추가될 수 있다. 이제 이를 nestia 를 활용해서 모든 swagger 코드를 제거하면 아래와 같이 가독성도 좋은 코드가 만들어진다.

/**
 * 채팅방 정보를 변경한다.
 */
@Put('rooms/:roomId')
@UseGuards(JwtGuard)
@HttpCode(200)
async postChangeRoom(/* params */) {
    /* logics */
}

 

위에서 단순히 한개의 controller 라우트 코드만을 만들었지만 개발하다보면 api 갯수가 100 여개를 넘기는 일은 흔한 일이 될 것이다. 위에서 라이브러리만 하나 추가해도 보기 힘들었던 controller 코드들이 싹 사라지도록 만들 수 있었다.

결론

초기에 nestia 는 sdk 를 편하게 만들어주는 라이브러리, 혹은 기존 nestjs 보다 빠른 라이브러리라고만 생각해서 마이그레이션 할 생각이 별로 없었다. sdk 는 모노레포 방식에서 사용하기가 조금 어려운 감이 있었고 현재 swagger 를 사용하는데 프런트에 추가적인 뭔가를 줘서 리펙토링 시키는 것은 너무 나간 이야기였다. 또 validating 관련 성능면에서 아직까지는 이슈가 없었기 때문에 현재 잘 쓰고 있는 것을 옮길 필요가 없었다. 하지만 swagger 를 직접 만드는 것에 대한 피로감, dto 클래스를 굳이 선언해서 확장이 어렵게 만드는 코드들 등 개발시에 피로할 수 있는 부분을 해결할 수 있지 않을까 해서 적용해 봤었는데 확실히 생산성이나 가독성 면에서 훨씬 좋아지는 효과가 있었다. 앞으로도 개발할 때는 이 라이브러리를 적극 활용해서 개발하지 않을까 싶다.