강의 들은 후기
- 강의명: [코드팩토리] [초급] NestJS REST API 백엔드 완전 정복 마스터 클래스 - Part 1 NestJS Core
- 강의 학습 기간: 일주일
- 이직하는 회사 입사 전 NestJS 에 대한 전체 강의를 듣고 내가 부족한 점이 무엇인지 찾고자 해당 강의를 들었습니다 :)
- 결론적으로 정말 듣길 잘한 것 같네요.. 원래 알았던 내용이여도 복습겸 듣기도 좋았고 제가 몰랐던 내용들도 정말 많이 알게 되어서 매우 유익하고 기쁩니다!
- 게다가 강사님이 설명을 야무지게 해주시네요. NestJS 주니어 개발자라면 한번쯤은 들어보라고 추천하고 싶은 강의였습니다.
강의를 들은 후 내가 얻은 기술 키워드
ServeStaticModule: 특정 폴더에 있는 모든 파일들을 외부에서 접근 가능하도록 하는 모듈
해당 모듈이 어메이징 한게 원래는 어떤 폴더에 대한 파일 response API 가 필요했을 텐데 해당 모듈을 app.module.ts 등록만 하면 파일을 가져올 수 있음.
imports: [ // 특정 폴더에 있는 모든 파일들은 외부에서 접근 가능하도록 하는 모듈 ServeStaticModule.forRoot({ // http://localhosT:3000/public/posts/9febf823-368a-4f62-ae8c-5813a2490e90.JPG rootPath: PUBLIC_FOLDER_PATH, serveRoot: '/public', }),
pagination: 데이터 정보를 한번에 보여주는 것이 아닌 일정 단위로 보내주는 기법
- Page Based Pagination
- 페이지 기준으로 데이터를 잘라서 요청하는 pagenation
- 요청을 보낼때 원하는 데이터 갯수와 몇번째 페이지를 가져올지 명시
- 페이지 숫자를 누르면 다음 페이지로 넘어가는 형태의 UI에서 많이 사용
- Pagenation 도중에 데이터베이스에서 데이터가 추가되거나 삭제될경우 저장되는 데이터가 누락되거나 중복될 수 있음.
- Pagenation 알고리즘이 매우 간단함.
- Cursor based Pagenation
- 가장 최근에 가져온 데이터를 기준으로 다음 데이터를 가져오는 Pagenation
- 요청을 보낼때 마지막 데이터의 기준값과 몇개의 데이터를 가져올지 명시
- 스크롤 형태의 리스트에서 자주 사용
- 최근 데이터의 기준값을 기반으로 쿼리가 작성되기 떄문에 데이터가 누락되거나 중복될 확률이 적음
- 위와 같이 페이징 기법에 대한 구현 방식도 볼수 있지만 이를 모듈화 하여 사용성을 높히는 작업도 하기 떄문에 학습에 매우 도움이 됨.
- 개인적으로 DTO 에서 쿼리 관련 필드를
{where or order}__{field name}__{method (optional)}
과 같은 형식으로 규격화 하여split('__')
한 후에 이를 기지고 FindOption을 만드는게 꽤 좋았음. - 해당 방법은 내가 앞으로 자주 사용하도록 노력하려고 함.
- 이유: 검색 조건이 변경될때에 따른 확장성이 너무 좋아보임.
- 개인적으로 DTO 에서 쿼리 관련 필드를
- Page Based Pagination
RBAC (Role Based Access Control)
- 권한에 따라 접근을 제한하는 방식이다. (ex. ADMIN/USER 에 따른 역할 분리)
- 예전의 나는 이를 모두 컨트롤러 레이터에서 로직을 구현하였다. 하지만 해당 강의에서는 reflect-metadata 를 이용한 RBAC 구현을 하였고 대부분의 로직을 데코레이터와 가드에서 구현하였다.
- 이러한 개념이 별도로 있다는걸 몰라서 상당히 흥미로웠고, 모든 로직을 컨트롤러 단에서 하는것 보단 가드에서 호출한 데이터의 권한을 확인해서 컨드롤러 로직을 타지 않게 하는게 제일 베스트 인것 같다.
reflect-metadata & Decorator
- 3번을 작성하면서 자연스럽게 나온 키워드인데 이거는 강좌에서 설명해준 것은 아니고 내가 별도로 정리를 했다.
- 내가 정리한 블로그 링크: https://alwns28.tistory.com/13
- 모든 기능이 유용해보이지는 않았지만 그래도 강좌의 RBAC 파트에서 사용된 부분은 정말 유용해 보였다. 그리고 property-decorator 나 accessor-decorator, method-decorator 이 세개는 유용해 보였다.
QueryRunner & Transaction
이전에는 트렌젝션에 대해서 잘 몰랐다. 이번 기회에 트렌젝션이 어떻게 사용되는지를 알게 되었다.
여러개의 DB 생성, 삭제 로직이 있을 경우에 중간에 예기치 못한 에러 발생 시 이전에 작업한 DB 로직에 대해서 rollback 을 해야 한다. 이러한 작업을 트렌젝션이 너무 잘 해내주고 있다.
// 예시로 언팔 기능에서 언팔 후에 팔로워, 팔로윙 수 decrement 로직을 수행 할 떄 트렌젝션을 사용하게 되면 간단하게 구현이 가능하다. @Delete('follow/:id') @UseInterceptors(TransactionInterceptor) async deleteFollow( @User() user: UsersModel, @Param('id', ParseIntPipe) followeeId: number, @QueryRunner() qr: QR, ) { await this.usersService.deleteFollow(user.id, followeeId, qr); await this.usersService.decrementFollowerCount(user.id, qr); await this.usersService.decrementFolloweeCount(followeeId, qr); return true; }
여기서 쿼리러너를 받은 방식은 request intercepter 에서 쿼리 러너 객체를 만들어서 req 객체 내에 저장해놓으면 이를 꺼내서 쓰는 방식이다.
// transaction.interceptor.ts import { DataSource } from 'typeorm'; import { CallHandler, ExecutionContext, Injectable, InternalServerErrorException, NestInterceptor, } from '@nestjs/common'; import { Observable, catchError, tap } from 'rxjs'; @Injectable() export class TransactionInterceptor implements NestInterceptor { constructor(private readonly dataSource: DataSource) {} async intercept( context: ExecutionContext, next: CallHandler<any>, ): Promise<Observable<any>> { const req = context.switchToHttp().getRequest(); // 트랜젝션과 관련된 모든 쿼리를 담당할 쿼리 러너를 생성한다. const qr = this.dataSource.createQueryRunner(); // 쿼리 러너에 연결한다. await qr.connect(); // 쿼리 러너에서 트랜잭션을 시작한다. // 이 시점부터 같은 쿼리 러너를 사용하면 // 트랜잭션 안에서 데이터베이스 액션을 실행 할 수 있다. await qr.startTransaction(); req.queryRunner = qr; return next.handle().pipe( catchError(async (e) => { await qr.rollbackTransaction(); await qr.release(); throw new InternalServerErrorException(e.message); }), // 에러가 발생할 때 실행되는 부분 tap(async () => { await qr.commitTransaction(); await qr.release(); }), ); } } // query-runner.decorator.ts import { ExecutionContext, InternalServerErrorException, createParamDecorator, } from '@nestjs/common'; export const QueryRunner = createParamDecorator( (data: any, context: ExecutionContext) => { const req = context.switchToHttp().getRequest(); if (!req.queryRunner) { throw new InternalServerErrorException( `QueryRunner Decorator를 사용하려면 TransactionInterceptor를 적용해야 합니다.`, ); } return req.queryRunner; }, );
ClassSerializerInterceptor 의 사용 방법
- ClassSerializerInterceptor 는 정말 유용한 기능이였다..
- ClassSerializerInterceptor 를 전역적으로 등록하고 response dto 들에다가 @Expose 또는 @Exclude 를 사용하면 인터셉터에서 해당 필드를 포함 또는 제외 시키고 데이터를 보내준다.
- 별도의 로직 구현 없이 이렇게 사용이 가능핟다는 점이 너무 편리하고 좋아보였다.
토큰 처리 방식에 대한 구현
- 나는 기존에는 passportStratege를 사용한 localStrategy, AccessTokenStrategy, RefreshTokenStrategy 를 구현했다. 그런데 해당 강의에서는 JwtService 만을 사용해서 토큰 파싱, 검증을 수행하게 구현햇다.
- 이를 비교하면서 좀 안목이 확장되는 느낌이 들었다.
- 내가 정리한 블로그 링크: https://alwns28.tistory.com/11
TypeORM Enum type Setting
TypeORM 의 entity 에서도 enum 타입을 사용할 수 있었다!!. 너무 간단한데 나는 왜 이걸 몰랐는가... 구현 방법을 보면 정말 간단하다..
export enum ImageModelType { POST_IMAGE, } @Entity() export class ImageModel extends BaseModel { ... @Column({ enum: ImageModelType }) @IsEnum(ImageModelType) @IsString() type: ImageModelType; ... }
강의를 들은 후 내가 얻은 노하우
AccessToken으로 인한 가드는 일단 모든 API 에 적용시키고, OpenAPI 로 설정할 API만 별도의 Public 로직을 구현하는 방식으로 가는게 보안성에 좋다.
// app.module.ts providers: [ AppService, { provide: APP_INTERCEPTOR, // NestJS APP 전체에서 해당 interceptor 가 적용된다. useClass: ClassSerializerInterceptor, }, { provide: APP_GUARD, // <-- AccessToken 가드를 전역적으로 등록 useClass: AccessTokenGuard, }, { provide: APP_GUARD, useClass: RolesGuard, }, ], })
고정 변수 관리 방법
본 강좌에서는 고정 변수가 필요한 경우 {이름}.const.ts 으로 파일을 만들어서 이를 선언하였다. 상당히 깔끔해 보여서 나도 이 방법을 자주 사용하려고 한다.
import { join } from 'path'; // 서버 프로젝트의 루트 폴더 export const PROJECT_ROOT_PATH = process.cwd(); // 외부에서 접근 가능한 파일들을 모아둔 폴더 이름 export const PUBLIC_FOLDER_NAME = 'public'; // 포스트 이미지들을 저장할 폴더 이름 export const POSTS_FOLDER_NAME = 'posts'; // 임시 폴더 이름 export const TEMP_FOLDER_NAME = 'temp'; // 실제 공개 폴더의 절대경로 export const PUBLIC_FOLDER_PATH = join(PROJECT_ROOT_PATH, PUBLIC_FOLDER_NAME); // posts 이미지를 저장할 전체 절대 경로 export const POST_IMAGE_PATH = join(PUBLIC_FOLDER_PATH, POSTS_FOLDER_NAME); // posts 이미지를 저장할 ROOT_PATH 를 제외한 경로 export const POST_PUBLIC_IMAGE_PATH = join( PUBLIC_FOLDER_NAME, POSTS_FOLDER_NAME, ); // 임시파일들을 저장할 폴더 export const TEMP_FOLDER_PATH = join(PUBLIC_FOLDER_PATH, TEMP_FOLDER_NAME);
Repository layer 다루는 방법
원래 내가 사용했던 방식은 DB 접근 로직이 간단하더라도 repository.ts 파일을 별도로 만들고 이를 서비스로직에서 injection 해서 사용하는 방식을 선호했다. (소스코드 관리 측면에서는 이게 좋다고 생각을 했었다.)
- 이게 좋은건가 라고 의구심이 들었던 계기는 나의 코드를 다시 봤을 때, 간단한 메소드만 사용하는 거라면 파일을 분리하는 것이 오히려 관리 포인트가 늘어나는게 아닐까 하는 생각이 들었다.
본 강의에서는 별도의 Repository 파일을 사용하지는 않고 @InjectRepository 를 이용하여 구현하였다. 이렇게 쓰는게 깔끔해 보이긴 한다...
@InjectRepository(PostsModel) private readonly postsRepository: Repository<PostsModel>,
Transaction 사용하는 로직에서 repository 셋팅하는 메소드 구현
와 이 함수는 간단한데 정말 유용하다. 트렌젝션을 사용할 경우와 사용하지 않을 경우에 대해서 repository 를 셋팅할 수 있는 메소드를 사용하는데 너무 유용했다. 이를 자주 사용할 것 같다.
getUsersRepository(qr?: QueryRunner) { return qr ? qr.manager.getRepository<UsersModel>(UsersModel) : this.userReporitory; } getUserFollowRepository(qr?: QueryRunner) { return qr ? qr.manager.getRepository<UserFollowersModel>(UserFollowersModel) : this.userFollowersRepository; }
class-validator 에서 error message 규격화
class-validator 에서 에러 메시지를 만들수 있는건 알고 있었는데 이를 규격화 할 수 있는 방법은 모르고 있었다.
@Column() @IsString({ message: stringValidationMessage, // <-- dto 또는 entity 에서 이렇게 message 에 method를 선언 }) title: string; // ################################# // 별도의 파일에서 이렇게 받은 데이터에 대한 에러 메시지를 다룰 수 있음. import { ValidationArguments } from 'class-validator'; export const stringValidationMessage = (arg: ValidationArguments) => { return `${arg.property}에 string을 입력해주세요.`; };
class-validator 의 옵션
아니 pipe 등록에서 유용한 옵션이 있다는걸 처음 알았다.
app.useGlobalPipes( new ValidationPipe({ transform: true, // dto에서 우리가 기본값을 넣어서 형성하려면 해당 옵션이 필요하다. transformOptions: { enableImplicitConversion: true, // 임의로 변환하는 것을 허가한다. ==> dto 에 선언된 타입 annotation 을 보고 자동으로 변환해준다. }, whitelist: true, // 우리가 정의한 DTO field 외에는 다른 field 를 가져오지 않는다. forbidNonWhitelisted: true, // 우리가 정의한 DTO 외에 다른 필드가 들어오면 에러를 밷는다. }), ); // global pipe
'NestJS' 카테고리의 다른 글
[NestJS] JWT 인증 가드 작성 방법에 대한 두가지 방법 및 나의 생각 (2) | 2023.12.26 |
---|---|
[NestJS] VScode 디버깅을 이용하여 똑똑하게 NestJS 를 디버깅하자! (0) | 2023.12.25 |
[NestJS] request lifeCycle 정리 (0) | 2023.11.11 |
[TypeScript] 타입스크립트 기초 (0) | 2023.11.05 |
[Rabbitmq] 에러 분석 2 - JavaScript heap out of memory 에러 (0) | 2023.10.23 |