๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Web Development/Back-end

Nestjs ํ•œ๋ˆˆ์— ๊ธฐ๋ณธ ์ •๋ฆฌ

๐Ÿ“Œ Nestjs ๊ธฐ๋ณธ๊ตฌ์กฐ


  • ESLint, Prettier๋Š” ์ฝ”๋“œ ์ปจ๋ฒค์…˜ ๊ทœ์น™์„ ์ž‘์„ฑํ•ด ํƒ€ ์ธ์› ๊ฐ„ ํ˜‘์—…ํ•  ๋•Œ ์ฝ”๋“œ ์ปจ๋ฒค์…˜์„ ๋งŒ๋“œ๋Š” ๊ณณ.
  • Decorator = ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์—ญํ• . (๋ถ™์—ฌ์จ์•ผํ•จ)
  • Controller = ๋“ค์–ด์˜ค๋Š” ์š”์ฒญ ์ฒ˜๋ฆฌ ๋ฐ ์‘๋‹ต๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์—ญํ• . ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ ํŠน์ • ์š”์ฒญ ์ˆ˜์‹  ๋ชฉ์ .
  • Request ๊ฐ์ฒด(@Req()). Body ๊ฐ์ฒด (@Body()). Param ๊ฐ์ฒด (@Param()).
  • Provider = ์„œ๋น„์Šค, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ, ํŒฉํ† ๋ฆฌ ๋“ฑ Nest์˜ Class๋“ค์€ ๋Œ€๋ถ€๋ถ„ ๊ณต๊ธ‰์ž๋กœ ์ทจ๊ธ‰๋œ๋‹ค. ์ด์— ๊ณต๊ธ‰์ž์—๊ฒŒ๋Š” ์ข…์†์„ฑ ์ฃผ์ž…์ด ๊ฐ€๋Šฅํ•˜๋‹ค.(Dependency Injection)
  • Controller (์†Œ๋น„์ž)๋Š” Service(๊ณต๊ธ‰์ž)๋ฅผ Injection๋ฐ›์•„์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค. (์˜์กด์„ฑ์ฃผ์ž…ํŒจํ„ด)
  • constructor(private readonly appService: AppService) {}
  • ๊ฐ๊ฐ์˜ Provider → Controller๋กœ DI์ด ์ง„ํ–‰๋˜๊ณ  → ์ด๊ฒƒ์ด ๋‹ค์‹œ App.module.ts๋กœ ํ•ฉ์ณ์ ธ์„œ main.ts์—์„œ ์‹คํ–‰๋œ๋‹ค.

๐Ÿ“Œ ์บก์Šํ™”


  • ๊ฐ์ฒด์˜ ์†์„ฑ๊ณผ Methods๋ฅผ ํ•˜๋‚˜๋กœ ๋ฌถ๊ณ  → ๊ตฌํ˜„๋œ ์ผ๋ถ€๋ฅผ ๊ฐ์ถ”์–ด ์€๋‹‰ํ•จ
  • ๋ชจ๋“ˆ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ณต๊ธ‰์ž๋ฅผ ์บก์Šํ™” ํ•œ๋‹ค. ๋•Œ๋ฌธ์— Exportํ•œ ๋ชจ๋“ˆ์— ๋Œ€ํ•ด์„œ๋งŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ์บก์Šํ™” ๋œ ๋ชจ๋“ˆ์„ ๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š” provider์— ์ค„์ค„์ด ์ถ”๊ฐ€ํ•˜์ง€ ๋ง๊ณ , exports ํ•ด์ฃผ๊ณ  → import ํ•ด์™€์„œ ์จ๋ผ. (๋‹จ์ผ์ฑ…์ž„์›์น™)

๐Ÿ“Œ ๋ฏธ๋“ค์›จ์–ด (nest g middleware <NAME>)


  • Route Handler (Controller) ์ด์ „์— ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜.
  • req, res, next๋กœ ๊ตฌ์„ฑ๋จ. (Express์™€ ๋™์ผ)
  • ๋ฏธ๋“ค์›จ์–ด ๋งŒ๋“ค๊ธฐ
  • // nest g middleware <NAME> import { Injectable, NestMiddleware } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; @Injectable() export class TestMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log("This is for test Middleware"); console.log(req.ip) next(); } }

๐Ÿ“Œ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ


  • App.module.tsํŒŒ์ผ์—์„œ ์•„๋ž˜์ฒ˜๋Ÿผ ์ ์šฉํ•จ.
  • apply(<์ ์šฉํ•  ๋ฏธ๋“ค์›จ์–ดํ•จ์ˆ˜>)
  • forRoutes(<์ ์šฉ๊ฒฝ๋กœ>)
export class AppModule **implements NestModule{
  configure(consumer : MiddlewareConsumer) {
    consumer.apply(TestMiddleware).forRoutes("*")
  }
}**

๐Ÿ“Œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ• (Exception Filter)

  • Nest๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์˜ˆ์™ธ ๋‚ด์šฉ์— ๋Œ€ํ•˜์—ฌ ์˜ˆ์™ธ ๋ ˆ์ด์–ด๋ฅผ ์ ์šฉ๋˜์–ด ์˜ˆ์™ธ์ฒ˜๋ฆฌ๊ฐ€ ์ž๋™์ ์œผ๋กœ ์ง„ํ–‰๋œ๋‹ค. ์ฆ‰, ๋ณ„๋„์˜ ์ฒ˜๋ฆฌ์—†์–ด๋„ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•˜๊ธด ํ•œ๋‹ค. ๋‹ค๋งŒ ์›ํ•˜๋Š” ์˜ˆ์™ธ์ฒ˜๋ฆฌ๊ฐ€ ์•„๋‹˜.
  • ๋งŒ์•ฝ ์—๋Ÿฌ๊ฐ€ ๋‚  ๋•Œ๋งˆ๋‹ค ์›ํ•˜๋Š” ํผ์˜ Err๋ฅผ ๋ฆฌํ„ดํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋˜๋Š” ์—๋Ÿฌ์— ๋Œ€ํ•œ ๋กœ๊ฑฐ๋ฅผ ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉดException Filter๋ฅผ ํ†ตํ•ด ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ์— ๋Œ€ํ•˜์—ฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.
  • ๊ธฐ๋ณธ์ ์ธ ์˜ˆ์™ธ ๋ฐœ์ƒ ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
@Get()
  getHello(): string {
    throw new HttpException('Forbidden2', HttpStatus.FORBIDDEN);
  }

์˜ˆ์™ธ Exception Filter ๋งŒ๋“ค๊ธฐ

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, 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 request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const error = exception.getResponse();

    // You Can Add Logger Hear;

    if (typeof error === 'string') {
        response.status(status).json({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            error: error
        })
    } else {
        response.status(status).json({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            ...error, // ๋น„๊ตฌ์กฐํ• ๋‹น
        })
    }

  }
}

- ์ „์ฒด์ ์šฉ

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

 

 

 

๐Ÿ“Œ ํŒŒ์ดํ”„

  • ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์— ์‚ฌ์šฉํ•˜๋Š” ํŒŒ์ดํ”„๋ผ์ธ Validation Check, Type๋ณ€ํ™˜์˜ ์—ญํ• ์„ ํ•ด์ค€๋‹ค.
// Param์— ํŒŒ์ดํ”„ ์ ์šฉ๋ฐฉ๋ฒ•
@Get(":id")
  getHello(@Param("id", **ParseIntPipe**) param): string {
    console.log(param);
    return this.appService.getHello();
  }
  • DTO์— ํŒŒ์ดํ”„์ ์šฉ๋ฐฉ์‹์€ ์ฃผ๋กœ class-validator ๋ฅผ ์ด์šฉํ•ด ์ง„ํ–‰ํ•œ๋‹ค.
// DTO Class ์ •์˜
import { IsNotEmpty, IsString, Matches, MaxLength, MinLength } from "class-validator";

export class SignInDto {
    @IsString()
    @IsNotEmpty()
    @MinLength(5)
    @MaxLength(50)
    @ApiProperty({ description : "Email" })
    email : string;

    @IsString()
    @IsNotEmpty()
    @Matches(/^[a-zA-Z0-9]*$/, {
        message : "Not Proper EOA"
    })
    @ApiProperty({ description : "EOA Public Key" })
    eoa : string;
}

// Controller ์ ์šฉ
@Post("/register")
registerUser(**@Body(ValidationPipe) registerUserDto : RegisterUserDto**) : Promise<User> {
    return this.userService.registerUser(registerUserDto);
}

 

๐Ÿ“Œ ์ธํ„ฐ์…‰ํ„ฐ

  • @Injectable ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๊ฐ€ ๋‹ฌ๋ฆฐ ํด๋ž˜์Šค๋กœ DI๊ฐ€ ๊ฐ€๋Šฅํ•œ ๊ฒƒ์ธ๋ฐ, ๊ฐ๊ฐ์˜ ๋ชจ๋“ˆ (Users, Markets)์„ ํšก๋‹จํ•˜๋ฉด์„œ ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ํ•จ์ˆ˜. (Middleware์™€ ์ƒ๋‹นํžˆ ์œ ์‚ฌํ•˜๋‹ค)
  • ๊ธฐ๋ณธ์ ์œผ๋กœ ์š”์ฒญ์ˆ˜๋ช…์ฃผ๊ธฐ(Request LifeCycle) ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
    • Request → Middleware → Guard → Intercepter (Pre) → Pipeline → Controller → Service → Intercepter (After) → Exception Filter → Response
  • ์œ„์—์„œ ์‚ดํŽด๋ณด๋ฉด Middleware์™€ Intercepter๋Š” ์ˆ˜๋ช…์ฃผ๊ธฐ ์ƒ ์ ์šฉ๋˜๋Š” ์‹œ๊ธฐ๊ฐ€ ๋‹ค๋ฅด๋‹ค. Intercepter๋Š” Pre์™€ After ๋กœ Controller ์‹คํ–‰ ์ด์ „ ์ดํ›„๋กœ ์ ์šฉ์ด ๋˜๋Š” ๋ฐ˜๋ฉด Middleware๋Š” Controller ๋™์ž‘ ์ „์—๋งŒ ์ ์šฉ.
  • ์ธํ„ฐ์…‰ํ„ฐ ๋งŒ๋“ค๊ธฐ (src/success.interceptor.ts)
  • import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class SuccessInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { console.log('Before...'); const now = Date.now(); return next .handle() .pipe( tap(() => console.log(`After... ${Date.now() - now}ms`)), ); } }
  • ์ ์šฉ๋ฐฉ๋ฒ•
    • ์‚ฌ์šฉํ•˜๋ฉด Before… → param → After ์ˆœ์œผ๋กœ ์ฝ˜์†”์— ๋œจ๊ฒŒ ๋œ๋‹ค.
  • @Get(":id") **@UseInterceptors(SuccessInterceptor)** getHello(@Param("id", ParseIntPipe) param): string { console.log(param); return this.appService.getHello(); }
  • ์–ด๋”ฐ์“ฐ๋ƒ? → ์—๋Ÿฌ์˜ ์ผ๊ด€์„ฑ์žˆ๋Š” ์ถœ๋ ฅ์„ ์œ„ํ•ด Exception Filter ์—์„œ ์‚ฌ์šฉํ–ˆ๋‹ค๋ฉด, Response์— ๋Œ€ํ•œ ์ผ๊ด€์„ฑ์žˆ๋Š” ๋ฆฌํ„ด์„ ์œ„ํ•ด Intercepter๋ฅผ ์ ์šฉํ•œ๋‹ค.

 

๐Ÿ“Œ Database Setting

  • ํ™˜๊ฒฝ๋ณ€์ˆ˜์„ค์ •
// Nestjs ์ž์ฒด ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ•ธ๋“ค๋ง ํŒจํ‚ค์ง€ ์ง€์› ์•„๋ž˜ ๋‚ด์šฉ์œผ๋กœ ์„ค์น˜ํ•˜๊ณ , .envํŒŒ์ผ์— ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฒ˜๋ฆฌ
npm i @nestjs/config 
  • DB ์—ฐ๊ฒฐ ์„ค์ •
// MySQL์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ฏ€๋กœ typeorm & mysql2 ์„ค์น˜
npm i @nestjs/typeorm mysql2

// app.module.ts - imports๋ถ€๋ถ„์— ์•„๋ž˜ ์ถ”๊ฐ€.
TypeOrmModule.forRoot({
  type : 'mysql',
  host : process.env.DATABASE_HOST,
  port : parseInt(process.env.DATABASE_PORT),
  username : process.env.DATABASE_USER,
  password : process.env.DATABASE_PASSWORD,
  database : process.env.DATABASE_DB,
  entities : [__dirname + "/entities/**/*.entity.{js,ts}"], // Entity ๊ฒฝ๋กœ
  synchronize : true // DB ๋™๊ธฐํ™”
}),

- Entity ์ •์˜

import { BaseEntity, Column, Entity, CreateDateColumn, UpdateDateColumn, OneToMany, PrimaryGeneratedColumn } from "typeorm";

@Entity({ 
    name : "users" // DB์— ์ƒ์„ฑ๋  Table ๋ช… ์„ค์ • ๊ฐ€๋Šฅ
})
export class User extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    email : string;

    @Column()
		@Exclude() // Return ๋•Œ, ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š” ํ•„๋“œ๋กœ ์˜ต์…˜ ์ถ”๊ฐ€ํ•จ.
    password : string;

    @Column({ nullable : false, unique : true, default : () => 0 })
    role : number;

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;
}

 

๐Ÿ“Œ DTO (Data Transfer Object)

  • ๊ณ„์ธต ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ตํ™˜ ๊ฐ์ฒด๋กœ Controller์— ์ „๋‹ฌ๋˜๋Š” ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•˜์—ฌ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Œ.
    • DTO ์ „์—ญ ์„ค์ • ํ›„ ์‚ฌ์šฉ๋ฒ•
    // @ main.ts ํŒŒ์ผ์— Global Validation Pipe ์‚ฌ์šฉ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. 
    // ๋งค ์ปจํŠธ๋กค๋Ÿฌ์— ์ ์šฉ๋„ ๊ฐ€๋Šฅํ•˜๋‚˜ ๊ธ€๋กœ๋ฒŒ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋ณด๋‹ค ํŽธํ•˜๋ฏ€๋กœ.
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      // Global Exception Filter ์—๋Ÿฌ ๋ฆฌํ„ด ํ•ธ๋“ค๋ง
      app.useGlobalFilters(new HttpExceptionFilter());
      **app.useGlobalPipes(new ValidationPipe());**
      // CORS ์„ค์ •
      app.enableCors();
    
      // Swagger
      await app.listen(process.env.SERVER_PORT);
    }
    bootstrap();
    
    // controller
      @Get("/")
      **signUpUser(@Body() body : SignupUserDto) {**
          console.log(body);
          return ["hello", "apple"];
      }
    
    • DTO ๊ฐœ๋ณ„ ์ ์šฉ๋ฐฉ๋ฒ•
      • ์•„๋ž˜์ฒ˜๋Ÿผ Body Decorator์— Validation Pipe๋ฅผ ๋„ฃ์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Œ.
    // ํšŒ์›๊ฐ€์ž…
    @Post("/register")
    registerUser(**@Body(ValidationPipe) registerUserDto : RegisterUserDto**) : Promise<User> {
        return this.userService.registerUser(registerUserDto);
    }
    

 

๐Ÿ“Œ PickType & OmitType

  • PickType์€ Class ์ •์˜ ์†์„ฑ ์ค‘ ์›ํ•˜๋Š” ๊ฒƒ์„ ์„ ํƒํ•ด ์ƒˆ๋กœ์šด ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ฆ‰ ํ•„์š”ํ•œ๊ฑธ ๊ณจ๋ผ์„œ ์ƒˆ๋กœ์šด ํƒ€์ž…์œ ํ˜•์„ ๋งŒ๋“ ๋‹ค.
  • OmitType์€ Class ์†์„ฑ ์ค‘ ํ•„์š”์—†๋Š” ํƒ€์ž…์„ ์ œ์™ธํ•˜๊ณ  ์ƒˆ๋กœ์šด ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ฆ‰ ํ•„์š”์—†๋Š”๊ฒƒ๋งŒ ๋นผ๊ณ  ์ƒˆ๋กœ์šด ํƒ€์ž…์œ ํ˜•์„ ๋งŒ๋“ ๋‹ค.
  • ์ฒ˜์Œ์—” ๊ทธ๋ƒฅ Promise์— ์ง์ ‘ ๋ฆฌํ„ด ํƒ€์ž… ๋„ฃ์–ด์„œ ํ•ด๋ดค๋‹ค. ๊ทผ๋ฐ ์ƒ๊ฐํ•ด๋ณด๋‹ˆ ์Šค์›จ๊ฑฐ ๋•Œ๋ฌธ์— ๋˜ ์ •์˜ํ•ด์•ผ๋˜๋”๋ผ. ๊ทธ๋Ÿฌ๋‹ˆ Promise์—๋‹ค๊ฐ€ ํƒ€์ž…์ •์˜ํ•˜์ง€๋ง๊ณ .. DTO์—๋‹ค๊ฐ€ ๋ฆฌํ„ด ์ •์˜ํ•ด ๋‘๊ณ  ์“ฐ๋Š”๊ฒƒ์ด ๋‚˜์„ ๊ฒƒ.

PickType Example

// Signup.dto.ts
...
import { User } from "src/entities/user.entity";

~~export class SignUpUserDto {}~~

// user entity ๋‚ด์šฉ ์ค‘ **'id', 'email', 'role'**๋งŒ ๋ฆฌํ„ดํ•˜๋Š” ํƒ€์ž…์„ ๋งŒ๋“ค๊ฒ ๋‹ค๋ฉด ?
**export class SignUpUserReturn extends PickType(
    User, ['id', 'email', 'role']
) {}**

OmitType Example

// Signup.dto.ts
import { ApiProperty, **PickType**, **OmitType** } from "@nestjs/swagger";
import { User } from "src/entities/user.entity";

~~export class SignUpUserDto {}~~

// user entity ๋‚ด์šฉ ์ค‘ Password๋งŒ ๋บด๊ณ  ๋ฆฌํ„ดํ•˜๋Š” ํƒ€์ž…์„ ๋งŒ๋“ค๊ฒ ๋‹ค๋ฉด ?
**export class SignUpUserReturn extends OmitType(
    User, ['password']
) {}**