본문 바로가기

FrontEnd+

nestjs 네스트 서버 기초 하루 만에 끝내기

코드 작성에 필요한 딱 '기초'만 정리해보자.

 

시작하기

다음 명령어를 통해 쉽게 nest 프로젝트를 세팅할 수 있다. 

$ npm i -g @nestjs/cli
$ nest new project-name

그러면 세팅 후 아래와 같은 파일들이 자동으로 생성된다. 각 모듈을 자체적인 전용 디렉토리에 보관하는 방식의 이 구조는 네스트에서 개발자들에게 권장하는 규칙이다.

src
    app.controller.ts  // route가 하나인 기본 컨트롤러
    app.controller.spec.ts  // 유닛테스트용
    app.module.ts  // 앱의 루트 모듈
    app.service.ts  // 메서드 하나인 기본 서비스
    main.ts  // 네스트 앱 인스턴스를 생성하기 위해 네스트의 핵심 함수 NestFactory를 사용하는 엔트리 파일

 

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);
}
bootstrap();

NestFactory는인스턴스를 생성할 수 있는 몇 가지 정적 메서드를 제공해, 이 클래스로 Nest 애플리케이션 인스턴스를 만들 수 있다. create() 메서드는 INestApplication 인터페이스에 부합하는 앱 객체를 리턴해준다.

위의 main.ts 예제 코드에서는 앱이 인바운드 HTTP 요청을 기다리도록 하는 HTTP 리스너를 시작해주기만 하면 된다.

 

컨트롤러

컨트롤러는 클라이언트의 요청을 받고, 응답을 리턴해주는 역할을 한다. 라우팅 메커니즘으로 어떤 요청을 어떤 컨트롤러가 처리할지 결정하게 된다.

보통 한 컨트롤러는 여러 라우트를 처리한다. 컨트롤러 안에서 각기 다른 라우트에 대해 다른 처리를 하게끔 할 수 있다.

이미지출처: https://docs.nestjs.com/controllers

 

컨트롤러 생성을 위해 클래스와 데코레이터를 사용한다. 데코레이터는 클래스, 메타데이터를 참고해서 네스트가 라우팅 맵을 만들 수 있도록 한다. 이 맵은 컨트롤러 내용에 맞게끔 요청을 묶어주는 용도이다.

CRUD 컨트롤러를 빠르게 만들려면 'nest g resource [name]'와 같은 CLI 명령어를 사용할 수 있다.

기본 컨트롤러를 정의하려면 @Controller 라는 데코레이터를 사용해주어야 한다. 

 

Routing

@Controller에 경로의 prefix만 넘기면 서로 연관있는 라우팅을 묶어서 처리하고 중복코드를 줄여준다. 다음 예제 코드는 'cats'로 시작하는 옵셔널 라우트 경로를 지정한 예시이다. 

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

 

위 예제코드에서 @Get() 은 HTTP 요청 메서드 get의 데코레이터로서, Nest에게 HTTP 요청에 대한 특정 엔드포인트에 대한 핸들러를  생성해달라고 전달한다. @Get() 뿐만 아니라 @Post() @Put() @Delete() @Patch() @Options() @Head() 모두 사용가능하다. 이 모두를 퉁쳐서 @All()로 쓸 수도 있다.

앞서 @Controller에서 정의한 prefix와 @Get에 넘기는 경로를 조합해서 최종적인 경로가 결정된다. 예제에서는 'cats' prefix는 주었지만 @Get에는 아무 path를 넘기지 않았으므로 Nest는 '/cat' 경로에 대한 GET 요청을 이 핸들러에 매핑하게 된다.

그 후 네스트는 이후에 작성된 메서드를 실행하고, 상태코드 200과 문자열을 응답을 리턴해준다.

 

Nest가 응답을 만들어주는 방식은 기본적으로 다음과 같다.

만약 자바스크립트 객체나 배열로 된 값을 핸들러에서 리턴하면, 자동으로 직렬화된 JSON을 반환한다. 그 외 원시타입을 반환하면 직렬화하지 않고 응답을 보낸다. 이 방식에서 응답코드는 200이 기본(POST 일 경우에만 201)이다. 이 방법이 기본이자 권장되는 방식이다.

응답코드를 수정하고 싶다면 @HttpCode(204)와 같이 데코레이터를 핸들러 레벨에 추가해주면 된다.

import { HttpCode, Header } from '@nestjs/common';

@Post()
@HttpCode(204)
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

 

Route parameters

GET /cats/1 과 같이 경로가 동적으로 변경되는 경우, @Get에 ':'와 함께 파라미터 토큰을 추가해주고, @Param() 데코레이터를 활용해 파라미터에 접근해서 동적으로 처리해줄 수도 있다.

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


@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

// Or

@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

 

Request Object

핸들러를 작성하다보면 클라이언트가 어떤 요청을 보냈는지에 대한 정보가 필요할 때가 있다. 이런 경우 @Req() 데코레이터를 함수 인자에 추가해서 Request 객체에 접근할 수 있다.

'@types/express'를 설치하면 express 타이핑을 활용할 수도 있다.

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';
  }
}

@Req에 접근하지 않고 @Param 나 @Body 처럼 요청 객체의 일부 필요한 값이 직접 접근할 수도 있다. 다음은 이를 가능하게 해주는 데코레이터 목록이다.

데코레이터
@Request(), @Req() req
@Response(), @Res() res
@Next() next
@Session() req.session
@Param(key?: string) req.params   /   req.params[key]
@Body(key?: string) req.body   /   req.body[key]
@Query(key?: string) req.query   /   req.query[key]
@Headers(name?: string) req.headers   /   req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

 

 

Request payloads

POST 에서 @Body() 데코레이터를 추가해보자. 타입스크립트를 사용한다면 'DTO' 스키마를 정해야 한다.

'DTO'는 Data Transfer Object의 약자로, 로직을 가지지 않고 게터/세터 메서드만 가진 클래스(객체)를 의미한다. 데이터가 네트워크를 통해 전송되는 방법을 정의하는 객체라고도 할 수 있다.

타입스크립트 인터페이스나 클래스로 DTO를 쉽게 작성할 수 있다. 타입스크립트 인터페이스는 트랜스파일 과정에서 제거되기 때문에 런타임에 네스트가 참조할 수 없지만, ES6 표준 문법인 클래스로 작성할 경우 런타임에도 네스트가 접근할 수 있기 때문에 파이프 등등 네스트의 기능에서 더 많은 것을 할 수 있게 되는 차이가 있다. 그래서 네스트 공식문서는 클래스로 작성할 것을 권장한다.

아래는 DTO의 간단한 예시이다.

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

이렇게 DTO 작성하고 컨트롤러 안에서 사용하면 된다.

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

 

Registration

컨트롤러 정의를 마치면 이 컨트롤러를 @Module() 데코레이터에 컨트롤러 배열을 추가해줘야 한다.  컨트롤러에 생명을 불어 넣으려면 항상 어떤 모듈에 추가해주어야 한다고 생각하면 쉽다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

위와 같이 루트 AppModule에 CatsController를 추가해주면, 비로소 네스트는 어떤 컨트롤러를 마운트해야하는지 알고, 이 클래스를 인스턴스화 할 수 있게 된다.

 

프로바이더

프로바이더 또한 네스트의 핵심 개념 중 하나이다. 프로바이더의 예시로는 'service', 'repository', 'factory', 'helper' 등이 네스트 클래스들이 있다. 

프로바이더는 의존성을 주입할 수 있도록 해준다. 이를 통해 인스턴스들을 서로 연결하는 기능은 거의 네스트의 런타임에 위임된다.

앞서 만든 간단한 컨트롤러는 HTTP 요청을 핸들링하고, 더 복잡한 작업이 있다면 이는 프로바이더에게 위임해주어야 한다. 

이미지 출처: https://docs.nestjs.com/providers

service

서비스는 데이터 저장이나 검색을 담당한다. CatsController에서 사용하는 서비스는 CatsService로 네이밍하면 되겠다. 

CatsService는 cats라는 하나의 프로퍼티와, create, findAll이라는 두 개의 메서드를 같는 아주아주 심플한 클래스다. 

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()이라는 데코레이터가 새로 사용되었다.

네스트에는 프로바이더 간의 관계를 결정하는 'IoC 컨테이너'가 있는데, @Injectable은 CatsService 서비스가 'IoC 컨테이너'에 의해 관리될 수 있다는 표시를 해준다.

이 서비스는 컨트롤러 안에서 다음과 같이 사용할 수 있다. CatsService가 constructor통해 주입되고 있다. 참고로 private 키워드로 선언과 초기화를 한방에 처리하고 있다.

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();
  }
}

 

Optional provider

때에 따라, 어떤 의존성을 반드시 IoC 컨테이너가 고려하지 않아도 되는 경우가 있을 수 있다. 예를 들어, 아무것도 전달되지 않은 경우 에러가 아니라 기본값을 사용하도록 해서 정상처리할 경우, 의존성은 optional이 된다.

프로바이더를 선택사항으로 두려면 프로바이더 생성자 매개변수에 @Optional 데코레이터를 추가해준다.

import { Injectable, Optional, Inject } from '@nestjs/common';

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

 

Registration

네스트가 의존성 주입을 할 수 있도록, 모듈에 이 내용을 등록해주어야 한다. controllers에 배열을 넣어준 것처럼 providers에 배열을 넘겨주면 된다.

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 {}

 

 

모듈

모듈은 @Module() 데코레이터로 표기하는 클래스이다. 모듈은 네스트가 앱의 '구조'를 구성할 수 있도록 메타데이터를 제공한다.

하나의 앱은 최소 하나 이상의 모듈을 갖는다. 이 때 최상위 모듈을 루트모듈이라고 한다.

네스트는 모듈, 프로바이더 관계 그리고 의존성을 결정하면서 내부적으로 데이터 구조를 그리는데 이를 앱 그래프(application graph)한다. 루트모듈은 네스트가 앱 그래프를 그려나가는 시작점이 된다.

이미지 출처: https://docs.nestjs.com/modules

이론적으로 앱의 크기가 작다면 모듈이 하나인 것도 가능하지만, 사실 일반적인 케이스는 아니다. 모듈을 여러 개 운용해서, 서로 밀접하게 관련된 기능들의 집합을 캡슐화하는 것이 더 효과적으로 컴포넌트를 관리하는 방법이자 강력하게 권장되는 방법이다.

@Module() 데코레이터는 providers, controllers, imports, exports 프로퍼티를 인자로 받을 수 있다.

CatsModule에서 controllers와 providers를 넣어주고, 다시 AppModule에서 CatsModule을 imports에 넣어주는 방식으로 사용할 수 있다.

// cats/cats.mdoule.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 {}

 

 

TypeORM

ORM이란 객체와 관계형 DB의 데이터를 자동으로 매핑해주는 것을 말한다. TypeORM은 TS, JS로 작성해서 노드, 일렉트론 등의 플랫폼에서 사용할 수 있는 ORM이다.

Repository pattern

TypeORM은 레포지토리 패턴을 지원한다. 각 entity는 각 repository를 갖는다. repository는 디비 커넥션을 통해 얻을 수 있다.

다음과 같은 Entity가 있다고 하자. (Entity에 대해 더 알고 싶다면 TypeORM 문서를 참고하자.)

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

이 User 라는 Entity를 사용하려면, 먼저 TypeORM이 알 수 있도록 루트모듈에서 forRoot() 메서드로 옵션을 추가해주어야 한다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

그리고 User 모듈에서도 forFeature()메서드로 현재 스코프에 등록된 레포지토리들을 정의해준다. 

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

이제 UsersService에서 @injectRepository() 데코레이터를 통해 UsersRepository를 주입할 수 있다.

// users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: string): Promise<User> {
    return this.usersRepository.findOne(id);
  }

  async remove(id: string): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

 

Relation

여러 테이블 간의 연결 관계를 릴레이션이라고 한다. 릴레이션은 보통 기본키(primary)와 외래키(foreign)를 포함하는 각 테이블의 공통 필드에 기반한다.

일대일 릴레이션이면 @OneToOne(), 일대다 릴레이션이면 @OneToMany(), 다대일 릴레이션이면 @ManyToOne(), 다대다 릴레이션이면 @ManyToMany() 데코레이터를 사용한다.

만약 User가 여러 photo를 가질 수 있다면 @OneToMany() 데코레이터를 사용하면 된다.

import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';

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

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToMany(type => Photo, photo => photo.user)
  photos: Photo[];
}

 

 

참고자료

https://docs.nestjs.com/first-steps

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com