Nest.js - typeORM 연결

김영훈·2022년 8월 20일
0

ETC

목록 보기
34/34
post-thumbnail

이번 글에선 typeORM 연결 및 migration, class-validator 설정 방법을 다룬다.

# entity 생성

  • typeorm-model-generator 설치

    • npm i typeorm-model-generator -D

    • ORM을 사용하려면 모델(model)이 필요하다. model은 일반적으로 객체와 데이터베이스를 매핑해주는 class를 의미한다. typeORM에선 entity라는 개념으로 존재한다. entity를 직접 생성하는 방법도 있지만, typeorm-model-generator 패키지를 활용하여 기존 데이터베이스를 토대로 entity를 자동 생성할 수도 있다. 비슷한 역할을 하는 패키지로, sequelize의 모델(model)을 자동으로 만들어주는 sequelize-auto 패키지가 있다.

  • entity 자동 생성

    • npx typeorm-model-generator -h localhost -d {databaseName} -u {username} -x {password} -e {engine}

      • 예시: npx typeorm-model-generator -h 127.0.0.1 -d clone_slack -i root -x 1q2w3e4r! -e mysql
    • entity 자동 생성 명령어를 입력하면, root 경로에 output 폴더가 생성된다. 해당 폴더 내부에 entities 폴더가 있는데, 폴더 자체를 그대로 복사하여 ./src 하위 경로에 붙여 넣는다.(entity 파일 저장 경로는 사용자기 원하는대로 설정해도 상관 없다. 필자의 경우 ./src/entities 하위 경로에서 관리하고 있다. )

    • 주의 사항

      • 자동 생성한 entity가 실제 DB와 오류 없이 매핑되려면 코드 수정이 필요하다.

        • createdAt 컬럼이 있다면, 자동 생성된 @Column() 대신 @CreateDateColumn()을 적용한다.

          // ./src/entities/Users.ts 
          @CreateDateColumn()
          createdAt: Date;
        • updatedAt 컬럼이 있다면, 자동 생성된 @Column() 대신 @UpdateDateColumn()을 적용한다.

          // ./src/entities/Users.ts 
          @UpdateDateColumn()
          createdAt: Date;
        • 중복된 index 데코레이터 삭제

          • 아래의 경우 처럼 동일한 index 데코레이터가 중복 생성될 수 있다. 이 중 한 가지를 삭제하여 중복을 제거한다.

            // ./src/entities/Workspaces.ts
            ...
            //@Index('IDX_22a04f0c0bf6ffd5961a28f5b7', ['url'], { unique: true }) <- 제거
            //@Index('IDX_de659ece27e93d8fe29339d0a4', ['name'], { unique: true }) <- 제거
            @Index('name', ['name'], { unique: true })
            @Index('OwnerId', ['ownerId'], {})
            @Index('url', ['url'], { unique: true })
            @Entity('workspaces', { schema: 'clone_slack' })
            export class Workspaces {
              @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
              id: number;
              ...
            }
            
  • swagger 데코레이터 적용

    • DTO를 쉽게 문서화하려면, 아래 예시처럼 entity 컬럼에 swagger 데코레이터 @ApiProperty() 적용을 적극 권장한다. nest.js에선 @Entity 자체가 DTO로 인식되기 때문에, 추후 DTO 생성 시 코드 중복을 줄일 수 있다.

          // ./src/entities/Users.ts
          ...
          @Entity('users', { schema: 'clone_slack' })
          export class Users {
            @ApiProperty({
              example: 1,
              description: '사용자 id',
            })
            @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
            id: number;
      
            @ApiProperty({
              example: 'fcfargo90@gmail.com',
              description: '사용자 이메일',
            })
            @Column('varchar', { name: 'email', unique: true, length: 30 })
            email: string;
            ...
          }
  • DTO 수정

    • nest.js에선 @Entity를 DTO로 인식한다고 얘기했다. 만약 위처럼 entity(Users) 객체에 swagger 데코레이터 적용을 완료했다면, 기존 Users관련 DTO를 아래와 같이 수정할 수 있다. 변수의 이름, type, swagger 데코레이터 등을 지정할 필요가 없어 코드 중복이 감소했다.

        // ./src/users/dto/join.request.dto.ts
      
        import { PickType } from '@nestjs/swagger';
        import { Users } from '../../entities/Users';
      
        export class JoinRequestDto extends PickType(Users, ['email', 'nickname', 'password'] as const) {}
        // ./src/users/dto/user.response.dto.ts
      
        import { PickType } from '@nestjs/swagger';
        import { Users } from '../../entities/Users';
      
        export class UserResponseDto extends PickType(Users, ['id', 'email', 'nickname', 'password'] as const) {}

# typeORM 연결

  • 패키지 설치

    • npm install --save @nestjs/typeorm typeorm mysql2

    • 설치 중 dependency conflict 오류가 발생할 경우, --force 명령어를 추가하면 된다.

  • TypeOrmModule 추가

    • app.module.ts에 아래 형식에 맞게 설정을 추가한다.

      • synchronize: true 옵션은 DB 연결 시 테이블이 자동 생성되므로 false로 관리하는 것이 안전하다.

      • entities: utoLoadEntities: true 옵션을 추가하면 일일이 entity 파일을 추가할 필요가 없지만, 해당 옵션 사용 시 버그가 발생하는 경우가 있기 때문에 아래처럼 작성했다.

      • keepConnectionsAlive: true 옵션을 설정할 경우 Hot reload 시 DB 연결을 유지해준다.

        TypeOrmModule.forRoot({
          type: 'mysql',
          host: 'localhost',
          port: 3306,
          username: process.env.DB_USERNAME,
          password: process.env.DB_PASSWORD,
          database: process.env.DB_DATABASE,
          entities: [Users, Channelchats, Channels, Channelmembers, Dms, Mentions, Workspacemembers, Workspaces],
          keepConnectionAlive: true,
          migrations: [__dirname + '/migrations/*.ts'],
          charset: 'utf8mb4',
          synchronize: false,
          logging: true,
        }),
    • app.module.ts 전체 코드

      // .src/app.module.ts
      import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
      import { ConfigModule, ConfigService } from '@nestjs/config';
      import { AppController } from './app.controller';
      import { AppService } from './app.service';
      import { LoggerMiddleware } from './middlewares/logger.middleware';
      import { UsersModule } from './users/users.module';
      import { DmsModule } from './dms/dms.module';
      import { ChannelsModule } from './channels/channels.module';
      import { WorkspacesModule } from './workspaces/workspaces.module';
      import { UsersService } from './users/users.service';
      import { TypeOrmModule } from '@nestjs/typeorm';
      import { Users } from './entities/Users';
      import { Channelchats } from './entities/Channelchats';
      import { Channelmembers } from './entities/Channelmembers';
      import { Channels } from './entities/Channels';
      import { Dms } from './entities/Dms';
      import { Mentions } from './entities/Mentions';
      import { Workspacemembers } from './entities/Workspacemembers';
      import { Workspaces } from './entities/Workspaces';
      
      @Module({
        imports: [
          ConfigModule.forRoot({ isGlobal: true }),
          TypeOrmModule.forRoot({
            type: 'mysql',
            host: 'localhost',
            port: 3306,
            username: process.env.DB_USERNAME,
            password: process.env.DB_PASSWORD,
            database: process.env.DB_DATABASE,
            autoLoadEntities: true,
            entities: [Users, Channelchats, Channels, Channelmembers, Dms, Mentions, Workspacemembers, Workspaces],
            keepConnectionAlive: true,
            migrations: [__dirname + '/migrations/*.ts'],
            charset: 'utf8mb4',
            synchronize: false,
            logging: true,
          }),
          UsersModule,
          DmsModule,
          ChannelsModule,
          WorkspacesModule,
        ],
        controllers: [AppController],
        providers: [AppService, ConfigService, UsersService],
      })
      export class AppModule implements NestModule {
        configure(consumer: MiddlewareConsumer): any {
          consumer.apply(LoggerMiddleware).forRoutes('*');
        }
      }
    • 서버 실행

      • npm run start:dev

      • synchronize 옵션을 true로 설정했다면 DB와 연결되는 동시에 테이블이 자동 생성될 것이고, 옵션을 false로 설정했다면 테이블 생성을 생략한 채 DB와 연결된다.

# Migration

  • DB에 새로운 테이블이나 컬럼을 추가할 때, 쿼리문으로 직접 DB를 정의해도 되지만, ORM의 migration 기능을 활용하면 훨씬 안정적인(실수가 발생했을 시 되돌리기가 가능하기 때문이다.) 작업이 가능하다. TypeORM migration 사용 방법은 다음과 같다.

  • dataSource.ts 생성

    • root 경로에 dataSource.ts 파일 생성

    • 아래 형식에 맞게 설정 추가

      // ./dataSource.ts
      
      import { DataSource } from 'typeorm';
      import dotenv from 'dotenv';
      import { Channelchats } from './src/entities/Channelchats';
      import { Channelmembers } from './src/entities/Channelmembers';
      import { Channels } from './src/entities/Channels';
      import { Dms } from './src/entities/Dms';
      import { Mentions } from './src/entities/Mentions';
      import { Users } from './src/entities/Users';
      import { Workspacemembers } from './src/entities/Workspacemembers';
      import { Workspaces } from './src/entities/Workspaces';
      
      dotenv.config();
      
      const dataSource = new DataSource({
        type: 'mysql',
        host: 'localhost',
        port: 3306,
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_DATABASE,
        entities: [Channelchats, Channelmembers, Channels, Dms, Mentions, Users, Workspacemembers, Workspaces],
        migrations: [__dirname + '/src/migrations/*.ts'],
        charset: 'utf8mb4',
        synchronize: false,
        logging: true,
      });
      
      export default dataSource;
  • package.json에 스크립트 추가

     #./package.json
    
     "scripts": {
       ...
       "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
       "schema:drop": "ts-node ./node_modules/typeorm/cli.js schema:drop",
       "schema:sync": "ts-node ./node_modules/typeorm/cli.js schema:sync",
       "db:migrate": "npm run typeorm migration:run -- -d ./dataSource.ts",
       "db:migrate:revert": "npm run typeorm -- -d ./dataSource.ts migration:revert",
       "db:create-migration": "npm run typeorm -- migration:create ./src/migrations/$npm_config_name",
       "db:generate-migration": "npm run typeorm -- migration:generate ./src/migrations/$npm_config_name -d ./dataSource.ts"
     },
  • migration 파일 생성

    • npm run db:create-migration --name={파일 이름}

      • 생성된 migration 파일은 dataSource.tsmigrations에서 지정한 경로에 저장된다.

      • 생성한 migration 파일에서 up 함수에는 DB 구조 변경에 사용할 정의문을 추가하고, down 함수에는 변경 사항을 되돌릴 때 사용할 정의문을 추가하면 된다.

        //.src/migrations/1661003507277-changeColumn
        
        import { MigrationInterface, QueryRunner } from 'typeorm';
        
        export class changeColumn1661003507277 implements MigrationInterface {
          public async up(queryRunner: QueryRunner): Promise<void> {
            await queryRunner.query(`ALTER TABLE users ADD COLUMN testColumn int`);
          }
        
          public async down(queryRunner: QueryRunner): Promise<void> {
            await queryRunner.query(`ALTER TABLE users DROP COLUMN testColumn`);
          }
        }
    • npm run db:migrate : migration 파일의 up 함수에서 작성한 쿼리문을 DB에 반영한다.

    • npm run db:migrate:revert : DB에 적용된 migration 파일의 down 함수에서 작성한 쿼리문을 DB에 반영한다.

    • npm run schema: drop : DB 테이블 모두 삭제

    • npm run schema: sync : entities 폴더 내부에 정의된 모든 @Entity를 DB에 반영

# Class-validator

  • nest.js에서 제공하는 class-validator를 활용하면 데이터 유효성 검사가 매우 편리해진다. api 요청(request) 시 넘겨받은 데이터의 타입이 DTO에서 정의된 것과 다를 경우, 별도의 로직 추가 없이 에러 코드를 반환할 수 있다.

  • 라이브러리 설치

    • npm i --save class-validator class-transformer
  • DTO에 데코레이터 추가

    • 앞에서 언급했듯 nest.js는 @Entity를 DTO로 인식하므로, entities 폴더 내부 entity 파일에 데코레이터를 추가한다.

      // ./src/entities/Users.ts
        ...
        @Entity('users', { schema: 'clone_slack' })
      export class Users {
        @ApiProperty({
          example: 1,
          description: '사용자 id',
        })
        @PrimaryGeneratedColumn({ type: 'int', name: 'id' })
        id: number;
      
        @IsEmail() // 이메일 타입이 아닌 경우 에러 코드를 반환한다.
        @ApiProperty({
          example: 'fcfargo90@gmail.com',
          description: '사용자 이메일',
        })
        @Column('varchar', { name: 'email', unique: true, length: 30 })
        email: string;
      
        @IsString() // 문자열 타입이 아닌 경우 에러 코드를 반환한다.
        @IsNotEmpty() // 빈 값을 넘겨받은 경우 에러 코드를 반환한다.
        @ApiProperty({
          example: 'fcfargo',
          description: '사용자 닉네임',
          required: true,
        })
        @Column('varchar', { name: 'nickname', length: 30 })
        nickname: string;
        ...
      }
  • Exception filter 생성

    • ./src 하위 경로에 httpException.filter.ts 파일 생성

    • 생성 파일에 아래 코드를 추가

      // ./src/httpException.filter.ts
      
      import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
      import { Response } from 'express';
      
      @Catch(HttpException)
      export class HttpExceptionFilter implements ExceptionFilter {
        catch(exception: HttpException, host: ArgumentsHost) {
          const ctx = host.switchToHttp();
          const response = ctx.getResponse<Response>();
          const status = exception.getStatus();
          const err = exception.getResponse() as { message: any; statusCode: number } | { error: string; statusCode: 400; message: string[] }; // class-validator 타이핑
      
          console.log(status, err);
          if (typeof err !== 'string' && err.statusCode === 400) {
            // class-validator 발생 에러
            return response.status(status).json({
              success: false,
              code: status,
              data: err.message,
            });
          }
      
          // 사용자 정의 에러(HttpException, BadRequestException 등..)
          response.status(status).json({
            success: false,
            code: status,
            data: err,
          });
        }
      }
  • ValidationPipe, HttpExceptionFilter 추가

    • 생성한 Exception filter(HttpExceptionFilter)가 controller 이후 실행될 수 있도록 main.ts를 설정한다. nest.js가 요청(request)을 처리 및 응답(response)하기까지의 과정(lifecycle)을 자세히 알고 싶다면 아래 링크를 확인하자.

  • main.ts 설정

    // ./src/main.ts
    
    import { NestFactory } from '@nestjs/core';
    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    import { AppModule } from './app.module';
    import { ValidationPipe } from '@nestjs/common';
    import { HttpExceptionFilter } from './httpException.filter';
    
    declare const module: any;
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // 포트 번호 정의
      const port = process.env.PORT || 8000;
    
      // exception filter
      app.useGlobalFilters(new HttpExceptionFilter()); // Exception filter 추가
    
      // class-validator
      app.useGlobalPipes(new ValidationPipe()); // ValidationPip 추가
    
      // sagger 문서 설정
      const config = new DocumentBuilder()
      .setTitle('Sleact API')
      .setDescription('Sleact 개발을 위한 API 문서입니다.')
      .setVersion('1.0')
      .addTag('connect.sid')
      .build();
      const document = SwaggerModule.createDocument(app, config);
      SwaggerModule.setup('swagger', app, document);
    
      await app.listen(port);
      console.log(`listening on port ${port}`);
    
      if (module.hot) {
        module.hot.accept();
        module.hot.dispose(() => app.close());
      }
    }
    bootstrap();

    이상의 과정을 마친 후 서버가 오류 없이 실행된다면, typeORM 연결 및 migration, class-validator 설정이 완료된 것이다.

profile
Difference & Repetition

0개의 댓글