NestJS 기본 공부

DongHyun Kim·2024년 8월 2일
0
post-custom-banner
  • Nest JS는 Javascript의 프레임워크로, 웹 백엔드 개발을 용이하게 해주는 역할을 한다.

💡사전 준비

  • NestJS 프로젝트를 실행할 수 있는 에디터 설치 (VScode 추천)
  • 16버전 이상의 Node.js 설치

✔️사전 지식 (필수 아님)

  • 스프링 기초 이상의 지식

Nest JS 세팅

  • Nest는 TS와 JS 모두 호환. 최신 언어 기능을 사용하기 위해 Javascript와 사용하려면 Babel 컴파일러가 필요.
  • npm을 이용해 초기 nest js 프로젝트 세팅하기
$ npm i -g @nestjs/cli
$ nest new project-name
  • 설치가 끝난 후 프로젝트 구조 설명
- node_modules
- src
	- app.controller.ts // 기본 컨트롤러, 단일 라우트
	- app.controller.spec.ts // 컨트롤러에 대한 단위 테스트 파일
	- app.module.ts // 애플리케이션의 루트 모듈
	- app.service.ts // 기본 서비스, 단일 메서드
	- main.ts // 애플리케이션 진입 파일, NestFactory를 사용하여 Nest 애플리케이션 인스턴스 생성
- test
// main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000); // 3000번 포트
}
bootstrap();
  • main.tsbootstrap (애플리케이션 초기화 + 비동기적 실행, Spring의 run과 비슷)
  • ****NestFactory 가 객체를 생성하는데 사용하는 클래스
    • create() 함수로 애플리케이션 객체를 반환
  • 다음 명령어로 실행 가능
$ npm run start
  • 빌드 속도를 높이려면 npm run -- -b swc 사용, swc 설명
  • 파일 변경 탐지로 재컴파일 하고싶으면 npm run start:dev

Controllers

  • 컨트롤러의 역할은 클라이언트의 HTTP 요청을 받아 적절한 응답을 반환
    • 스프링과 비슷하기 때문에 MVC 패턴을 보면 될듯함
  • 기본 컨트롤러는 클래스 형식이며, 데코레이터를 사용한다.
    • 데코레이터는 @Controller(), @Get()와 같이 스프링의 어노테이션 역할

Routing

  • 어떤 컨트롤러가 어떤 요청을 받는지 제어
  • 스프링의 Mapping 어노테이션과 비슷
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}
  • Controller 데코레이터가 cats 경로에 요청 처리
  • 모든 라우트가 /cats 로 경로 시작
  • 위 코드에선 /cats 로 Http Get 요청을 보내면 findAll() 메서드 에서 처리됨

응답(response) 조작

  • Standard (기본, 추천)
    • 객체나 배열과 같은 경우 자동으로 JSON으로 직렬화
    • 원시 타입의 경우 해당 값만 보냄
    • Http 상태 코드는 @HttpCode() 로 변경 가능
  • Library (Express와 같은 타 라이브러리)
    • @Res() 와 같은 데코레이터를 주입하면 기본 응답을 무시하고 커스텀하여 사용도 가능

Request object

  • @Req() 데코레이터를 파라미터에 이용
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}
  • 다양한 요청 객체에 접근 가능 - 공식문서 Request Object

  • 요청 데코레이터 테스트 코드

    import { 
      Controller,
      Get,
      Post,
      Req,
      Res,
      Session,
      Param,
      Body,
      Query,
      Headers,
      Ip,
      HostParam,
      HttpCode,
    } from '@nestjs/common';
    import { AppService } from './app.service';
    import { Request, Response } from 'express';
    
    @Controller()
    export class AppController {
      constructor(private readonly appService: AppService) {}
    
      @Get()
      getHello(): string {
        return this.appService.getHello();
      }
    
      @Get('test/:id')
      exampleGetMethod(
        @Req() req: Request,
        @Res() res: Response,
        @Session() session: any,
        @Param('id') id: string,
        @Query('name') query: any,
        @Headers() headers: any,
        @Ip() ip: string,
        @HostParam() hosts: any,
      ): void {
        // 각 요청 데이터를 출력
        console.log('Request URL:', req.url);
        console.log('Session:', session);
        console.log('Param id:', id);
        console.log('Query:', query);
        console.log('Headers:', headers);
        console.log('IP Address:', ip);
        console.log('Hosts:', hosts);
    
        // 응답 보내기
        res.status(200).send('success');
      }
    
      @HttpCode(201)
      @Post('test')
      examplePostMethod(
        @Req() req: Request,
        @Session() session: any,
        @Param('id') id: string,
        @Body() body: any,
        @Query() query: any,
        @Headers() headers: any,
        @Ip() ip: string,
        @HostParam() hosts: any,
      ) {
        // 각 요청 데이터를 출력
        console.log('Request URL:', req.url);
        console.log('Body:', body);
    
        // 응답 보내기
        return 'post success';
      }
    }
    
  • 응답 결과

    // GET 요청
    Request URL: /test/1?name=kim
    Session: undefined
    Param id: 1
    Query: kim
    Headers: {
      'user-agent': 'PostmanRuntime/7.40.0',
      accept: '*/*',
      'postman-token': '...',
      host: 'localhost:3000',
      'accept-encoding': 'gzip, deflate, br',
      connection: 'keep-alive'
    }
    IP Address: ::1
    Hosts: {}
    
    // POST 요청
    Request URL: /test
    Body: { name: 'kim' }

    Untitled

    Untitled

Sub-Domain Routing

  • 특정 도메인에서만 요청할 수 있도록 Sub-Domain 설정 가능
@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get('api/admin')
  getAdminApi(): string {
    return 'This is the admin API';
  }
}

Reqeust Payload

  • DTO를 이용한 데이터 스키마 이용한 요청 Body 받기
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

import { Controller, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './create-cat.dto';

@Controller('cats')
export class CatsController {
  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }
}

Providers

  • Service, Repository, Factory, Helper 클래스 등등이 Provider 역할
  • 다른 클래스에 주입 (DI) 될 수 있다

Services

  • CatService를 만들어서 CatsController에 주입하는 과정
  • CatService는 데이터를 저장하고, 가져오는 역할
// cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}
  • @Injectable() 을 사용하여 Nest IoC 컨테이너에서 관리할 수 있는 클래스임을 선언
// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
  • 생성자에 private 를 붙여서 chatService를 선언과 동시에 주입받을 수 있도록 함
    • 이 방법을 사용하면 자동으로 catsService를 클래스 프로퍼티로 선언하고 초기화한다 ❗

Optional providers

  • 주입받지 못해도 오류를 발생시키지 않도록하는 데코레이터. 주입받을 것이 없어도 오류가 발생하지 않는다 (null 또는 undefined로 남는다)
import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

Property-based injection

  • 속성에서 주입받는 방법
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}
  • 다만 생성자 주입이 더 가시성이 좋고 명시적이기 때문에 생성자 주입 방식을 선호한다

Provider registration

  • CatsService (Provider) 를 정의했고 CatsController 에 주입한다고 표시했으므로, 서비스를 Nest에 등록하여 인젝션을 수행할 수 있도록 해야한다.
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}
  • @Moduel 데코레이터를 통해 공급자 배열에 서비스 추가

Modules

  • 모듈은 @Module() 데코레이터가 붙은 클래스
  • Root Module이 하위 Module들을 관리하고 하위 Module은 각 Feature Module을 가진다.
  • providers: Nest Injector에 의해 인스턴스화되고, 현재 모듈에서 공유될 provider 집합
// cats/cats.module.ts (하위 모듈)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}
// app.module.ts (루트 모듈)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

Shared modules

  • 어떤 객체를 다른 모듈에서도 공유해서 쓰고싶을 때, 아래와 같이 exports 을 배열에 추가하면 CatsModule을 import하는 곳에서 CatsService를 사용할 수 있다.
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

Global modules

  • 모듈을 여러 군데에서 필요하다면 Angular Provider인 @Global() 데코레이터 사용. 헬퍼, 데이터 모듈과 같은 여러 곳에서 필요한 것에 유용할 수 있다.
  • Global 모듈은 루트나 코어 모듈에 한 번만 등록되어야한다.
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

SQL (TypeORM)

  • TypeORM , postgresql 설치
$ npm install --save typeorm pg
  • postgresql 설치

데이터베이스와 연결하기

  • new DataSource().initialize() 를 이용해서 데이터베이스와 연결, DataSource의 매개변수로 DB 연결 정보 넣기
import { DataSource } from 'typeorm';

export const databaseProviders = [
  {
    provide: 'DATA_SOURCE',
    useFactory: async () => {
      const dataSource = new DataSource({
        type: 'mysql',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: 'root',
        database: 'test',
        entities: [
            __dirname + '/../**/*.entity{.ts,.js}',
        ],
        synchronize: true,
      });

      return dataSource.initialize();
    },
  },
];
  • initialize() 는 Promise를 반환하기 때문에 async로 실행해야한다.
  • entities는 테이블과 매핑되는 엔티티 파일의 경로
  • synchronize를 true로 하면 데이터베이스 스키마가 엔티티 정의와 자동으로 동기화

데이터베이스 Provider 접근

  • databaseProvider는 DatabaseModule로 감싸서 외부에 노출시키는게 관례이다.
import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}
  • 이제 @Inject() 데코레이터를 이용해 DATA_SOURCE 객체를 주입할 수 있다.

Repository pattern

  • 스프링과 유사하게 데이터베이스와 밀접한 클래스를 Repository로 분리,
    아래 예시를 통해 학습할 수 있다.
  • Photo 디렉토리를 대표하는 PhotoModule이 있다.
  • Photo Entity 생성
// photo.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Photo {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 500 })
  name: string;

  @Column('text')
  description: string;

  @Column()
  filename: string;

  @Column('int')
  views: number;

  @Column()
  isPublished: boolean;
}
// photo.provider.ts
import { DataSource } from 'typeorm';
import { Photo } from './photo.entity';

export const photoProviders = [
  {
    provide: 'PHOTO_REPOSITORY',
    useFactory: (dataSource: DataSource) => dataSource.getRepository(Photo),
    inject: ['DATA_SOURCE'],
  },
];
  • 참고: 위에 useFactory와 같은 Factory Provider를 이용한다면 @Injectable 데코레이터가 없어도 inject 배열에 있는 객체를 의존성 매개변수에 주입받을 수 있다. (userFactory를 사용해 Photo 엔티티에 대한 Repository 반환)
  • 이제 PHOTO_REPOSITORY를 주입할 수 있다
// photo.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Photo } from './photo.entity';

@Injectable()
export class PhotoService {
  constructor(
    @Inject('PHOTO_REPOSITORY')
    private photoRepository: Repository<Photo>,
  ) {}

  async findAll(): Promise<Photo[]> {
    return this.photoRepository.find();
  }
}
  • 생성한 Provider와 Service를 모듈에 등록
// photo.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { photoProviders } from './photo.providers';
import { PhotoService } from './photo.service';

@Module({
  imports: [DatabaseModule],
  providers: [
    ...photoProviders,
    PhotoService,
  ],
})
export class PhotoModule {}

@InjectRepository 데코레이터

  • InjectRepository 데코레이터를 쓴다면 Provider를 생략할 수 있다

@Injectable()
export class PhotoService {
  constructor(
    @InjectRepository(Photo)
    private photoRepository: Repository<Photo>,
  ) {}
}

TypeORM의 EntityManager, QueryBuilder - SQL 작성하기

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, EntityManager } from 'typeorm';
import { MyEntity } from './my.entity';

@Injectable()
export class AppService {
  constructor(
    @InjectRepository(MyEntity)
    private myEntityRepository: Repository<MyEntity>,
    private entityManager: EntityManager,
  ) {}

  // EntityManager 사용
  async runCustomQueryWithEntityManager() {
    const rawData = await this.entityManager.query('SELECT * FROM my_entity');
    return rawData;
  }

  // QueryBuilder 사용
  async runCustomQueryWithQueryBuilder() {
    const rawData = await this.myEntityRepository
      .createQueryBuilder('entity')
      .select('entity.id')
      .addSelect('entity.name')
      .where('entity.age > :age', { age: 25 })
      .getRawMany();
    return rawData;
  }
}

간단한 CRUD 프로젝트

  • 유저를 생성, 조회, 수정, 삭제 하는 간단한 프로젝트를 진행하여 앞에서 배웠던 내용들을 복습해봅니다. https://github.com/Anak-2/nest-project
  • 전체 코드는 위를 보면 확인할 수 있습니다. 아래 내용은 코드를 작성하면서 겪었던 트러블 슈팅과 헷갈렸던 부분을 정리하며, 위에서 부족했던 내용을 보충합니다.

src/user 폴더

// user.controller.ts
@Controller('user')
export class UserController {
    constructor(private readonly userService: UserService) {}

    @Post('create')
    create(@Body() userCreateRequest: UserCreateRequest): void {
        this.userService.doCreate(userCreateRequest);
    }

    @Get('read/:id')
    async read(@Param('id') id: number): Promise<UserReadResponse> {
        return await this.userService.doRead(id);
    }

    @Patch('update')
    update(@Body() userNameUpdateRequest: UserNameUpdateRequest): void {
        return this.userService.doUpdate(
            userNameUpdateRequest.id,
            userNameUpdateRequest.phone,
        );
    }

    @Delete('delete/:id')
    async delete(@Param('id') id: number): Promise<boolean> {
        return await this.userService.doDelete(id);
    }
}
  • 클라이언트의 요청을 받아서 로직을 매핑해주는 Controller입니다. 스프링과 유사한 부분이 많아 자세한 설명은 넘기겠습니다.
  • DTO를 이용해 요청 데이터를 매핑합니다.
// user.service.ts
@Injectable()
export class UserService {
    @Inject('USER_REPOSITORY')
    private userRepository: Repository<UserEntity>;

    doCreate(userCreateRequest: UserCreateRequest): void {
        // console.log(userCreateRequest instanceof UserCreateRequest);
        // 자바스크립트는 클래스의 인스턴스가 아닌 단순한 객체를 넘겨주기 때문에 class transfromer를 이용해야 클래스 내부의 메서드를 이용할 수 있다
        const userCreateClass = plainToClass(
            UserCreateRequest,
            userCreateRequest,
        );
        this.userRepository.save(userCreateClass.toEntity());
    }

    async doRead(id: number): Promise<UserReadResponse> {
        const userEntity = await this.userRepository.findOne({ where: { id } });
        const userReadResponse = new UserReadResponse();
        userReadResponse.name = userEntity.name;
        userReadResponse.phone = userEntity.phone;
        return userReadResponse;
    }

    // update 함수는 첫 번째 인수 조건에 맞는 Entity를 찾아서 두 번째 인수의 Object 값으로 업데이트
    doUpdate(id: number, phone: string): void {
        const phoneObj = { phone: phone };
        this.userRepository.update({ id: id }, phoneObj);
    }

    async doDelete(id: number): Promise<boolean> {
        return (await this.userRepository.delete({ id: id })) ? true : false;
    }
}
  • 트러블 슈팅
    1. doCreate 부분에 userCreateRequest DTO를 매개변수로 받아서, 클래스 내에 정의한 toEntity() 함수를 이용해 UserEntity로 만들려했습니다.
      하지만 Typescript임에도 불구하고 자바스크립트는 단순 객체만 넘겨받는 성질 때문에 class transformer 라이브러리의 plainToClass 함수를 이용해야했습니다.
    2. Typescript에선 async 함수의 반환값은 Promise 객체로 감싸야합니다.
    3. TypeORM의 repository 기본 제공 함수의 기능으로 복잡한 쿼리가 가능한지 연구가 필요합니다.
    4. ‘USER_REPOSITORY’ 를 주입받을 때 오타가 나서 주입받지 못한 오류가 있었습니다. 매직 스트링을 사용하지 않고 상수로 관리하길 권장합니다.
  • 공식문서에서 Database보다 TypeORM을 우선 학습했더니 @InjectRepository 데코레이터 방식이 아닌 Provider로 주입받은 방식을 이용했습니다. 더 간편한 것은 @InjectRepository 방식인 것 같으니 바꿔보는 과정이 필요합니다.
// user.provider.ts
export const userProviders = [
    {
        provide: 'USER_REPOSITORY',
        useFactory: (dataSource: DataSource) =>
            dataSource.getRepository(UserEntity),
        inject: ['DATA_SOURCE'],
    },
];
  • provider를 이용해 USER_REPOSITORY를 주입할 수 있도록 생성해줍니다.
  • useFactory를 이용하면 @Inject 데코레이터를 생략하고 객체를 주입받을 수 있습니다. 주입할 객체는 inject 배열에 주입할 순서를 지켜서 적으면 됩니다.
// user.module.ts
@Module({
    imports: [DatabaseModule],
    controllers: [UserController],
    providers: [UserService, ...userProviders],
    exports: [UserService],
})
export class UserModule {}

// app.module.ts
@Module({
    imports: [UserModule, DatabaseModule],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}
  • UserModule을 만들어서 AppModule에 등록해줘야 api가 클라이언트에 노출됩니다. ⭐
  • imports를 이용해 외부 모듈을 가져와서 주입받을 수 있도록 합니다.
  • UserService와 userProviders를 providers에 넣어서 주입 컨테이너에서 관리하도록 만듭니다.
  • UserService를 외부에서도 주입받을 수 있도록 exports 합니다.
profile
do programming yourself
post-custom-banner

0개의 댓글