Skip to content

0018.1 Nest.js ๐Ÿฑ


README#

NestJs ํ•™์Šต์„ 2023-11-10~2023-11-11 ์ดํ‹€๊ฐ„ ์ง„ํ–‰ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ๊ป๋ฐ๊ธฐ๋งŒ ์กฐ๊ธˆ ํ›‘๋Š”๋‹ค๋Š” ๋Š๋‚Œ์œผ๋กœ ๊ฐ€๊ณ  ๋‚˜๋จธ์ง€๋Š” ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ฑ„์›Œ๋„ฃ์–ด๋ณด์ž.

Daily Dump#

2023-11-13#

Overview#

  • NestJS ์„ค์น˜๋ฒ•:
    • npm i -g @nestjs/cli
    • nest new project-name
    • nest start
  • ํ•ต์‹ฌ ํŒŒ์ผ ๊ตฌ์กฐ
    • app.controller.ts: ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋ผ์šฐํ„ฐ์ž„
    • app.controller.spec.ts: spec์ด๋ผ๋Š” ์ด๋ฆ„์€ ์œ ๋‹›ํ…Œ์ŠคํŠธ๋ฅผ ์˜๋ฏธํ•จ.
    • app.serivce.ts: ์„œ๋น„์Šค๋Š” ์ปจํŠธ๋กค๋Ÿฌ์— ์˜ํ•ด ํ˜ธ์ถœ๋˜๋Š” ๋‹ค์–‘ํ•œ ์ฝœ๋ฐฑ๋“ค์„ ๋‹ด์•„๋†“๋Š” ํŒŒ์ผ
    • app.module.ts: ํ”„๋กœ์ ํŠธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ณณ
    • main.ts: ์—”ํŠธ๋ฆฌํŒŒ์ผ, app ์ƒ์„ฑํ•จ
  • ๋น ๋ฅด๊ฒŒ ์ปดํŒŒ์ผํ•˜๊ณ  ์‹คํ–‰์‹œํ‚ค๋ ค๋ฉด... npm run start -- -b swc
  • hot reloading์„ ์ž์ฒด์ ์œผ๋กœ ์ง€์›ํ•œ๋‹ค : npm run start:dev
  • CRUD ๊ฐ€๋Šฅํ•œ ๋ชจ๋“ˆ (controller, module, service, dto, entity)์„ ๋งŒ๋“œ๋Š” ๋ช…๋ น์–ด๋Š” nest g res <resource-name>, ๋‹ค์Œ ์„ธ ๋ช…๋ น์–ด๋ฅผ ์ผ๊ด„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š”๋“ฏ.
    • nest g controller
    • nest g service
    • nest g module

Controllers#

  • request & response๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ ˆ์ด์–ด.
  • ์ƒˆ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ช…๋ น์–ด๋Š” nest g controller <name>์ด๋‹ค.
  • ํด๋ž˜์Šค ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ @Controller(<path>)๊ฐ€ ๋‹ฌ๋ ค์žˆ๋‹ค.
  • ๋ฉ”์„œ๋“œ๋“ค์ด ๊ฐ๊ฐ์˜ ์—”๋“œํฌ์ธํŠธ๋กœ ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•ด์„  @GET([subpath])์™€ ๊ฐ™์€ HTTP ๋ฉ”์„œ๋“œ ์ด๋ฆ„์˜ ์–ด๋…ธํ…Œ์ด์…˜์„ ๋‹ฌ์•„์•ผ ํ•œ๋‹ค.
  • ์„ฑ๊ณต์ ์œผ๋กœ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ด์•ผํ•  ๋•Œ ๊ธฐ๋ณธ๊ฐ’์€ 200์ด๊ณ , POST์ผ ๊ฒฝ์šฐ 201์ด๋‹ค. ์ด๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜๊ธฐ ์œ„ํ•ด์„  @HttpCode(<number>) ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
    • ์—๋Ÿฌ status๋ฅผ ๋ฐ˜ํ™˜ํ•  ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ throwํ•˜๋ผ๋˜๋ฐ?
    • ์•„๋ฌด status๋‚˜ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์‹ถ์„๋• express์˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ผ๊ณ ..

  • Request Object

    • Path ์ธ์ž๋Š” @Get(:id) ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ •์˜ํ•˜๊ณ  ํŒŒ์‹ฑ๋œ ์ธ์ž๋Š” ๋ฉ”์„œ๋“œ ์ธ์ž๋กœ ๋“ค์–ด์˜จ๋‹ค.
    @Get(':id')
    fineOne(@Param() params: any): string {
        return param.id + 'cat';
    }
    
    • request ์„ธ๋ถ€์‚ฌํ•ญ์„ ์•Œ๊ธฐ ์œ„ํ•ด์„  ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฉ”ํ„ฐ ์•ž์— @Req() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋ถ™์ด๋ผ๊ณ !
    import { Request } from 'express';
    @Get()
    fineOne(@Req() request: Request): string {
        return request.body;
    }
    
  • ์ด๋ ‡๊ฒŒ ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฉ”ํ„ฐ๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋“ค๋กœ๋Š” @Res, @Next, @Session, @Param, @Body, @Query, @Headers ๋“ฑ์ด ์žˆ๊ณ  ๋‹ค์Œ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ์˜ค.

@Param#

๋™์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” Path์ธ์ž. /cat/1์—์„œ์˜ 1๊ณผ ๊ฐ™์€ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋งค๊ฐœ ๋ณ€์ˆ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ๋กœ๋Š” ์ •์  ๊ฒฝ๋กœ ๋’ค์— ์„ ์–ธ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๋งค๊ฐœ ๋ณ€์ˆ˜ํ™”๋œ ๊ฒฝ๋กœ๊ฐ€ ์ •์  ๊ฒฝ๋กœ๋กœ ํ–ฅํ•˜๋Š” ํŠธ๋ž˜ํ”ฝ์„ ๊ฐ€๋กœ์ฑ„๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

@Query#

? ์ดํ›„์— ๋‚˜์˜ค๊ณ  &๋กœ ๋ถ„๋ฆฌ๋œ ์ธ์ž๋“ค์„ ์˜๋ฏธ. key=value ์Œ์œผ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋‹ค.

  • ์˜ˆ์‹œ: /cat?key1=value1&key2=value2
  • deferred response๋ฅผ ๋ฆฌํ„ดํ•ด๋„ ๋œ๋‹ค๊ณ . Promise ํƒ€์ž… ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•˜๊ฒŒ ๋˜๋ฉด #RxJS์˜ observable stream์œผ๋กœ ๋ฆฌํ„ด์ด ๋˜๊ณ , ์ŠคํŠธ๋ฆผ์ด ๋๋‚ ๊ฒฝ์šฐ ํ•ด๋‹น ์†Œ์Šค์— ์•Œ๋ฆผ์ด ๊ฐ„๋‹ค๊ณ ๋Š” ํ•˜๋Š”๋ฐ ๋ญ” ์†Œ๋ฆฐ์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Œ.

  • Request Payloads
    • DTO, Data Transfer Object๋ผ๊ณ  ๋ถˆ๋ฆฌ์šฐ๋Š”, ๋„คํŠธ์›Œํฌ ์ „์†ก์„ ์œ„ํ•ด ์กด์žฌํ•˜๋Š” ๊ฐ์ฒดํƒ€์ž…์€ ํด๋ž˜์Šค๋กœ ์ •์˜๊ฐ€ ๋˜์–ด์žˆ๋‹ค๊ณ . DTO ํด๋ž˜์Šค ๋ฉค๋ฒ„๋“ค์€ ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ํƒ€์ž…๋งŒ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋‚˜?
    • express.js์—์„œ POST์š”์ฒญ์— ๋Œ€ํ•œ ์š”์ฒญ์˜ body๋ฅผ ๋œฏ์–ด ์›ํ•˜๋Š” ํ‚ค๊ฐ’๋“ค์„ ์ง์ ‘ ํš๋“ํ–ˆ๋‹ค๋ฉด, Nest.js๋Š” DTO ๊ฐ์ฒด๋ฅผ ์ธ์ž๋กœ ๋ฐ›์•„์™€ ๊ณง๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    • @Post() create(@Body() createCatDto: CreateCatDto) {...}
  • app.module.ts ํŒŒ์ผ์— ๋‚ด๊ฐ€ ์ •์˜ํ•œ ์ปจํŠธ๋กค๋Ÿฌ๋“ค์„ @Module() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์•ˆ์— ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค.

Providers#

์˜์กด์„ฑ ์ฃผ์ž…๋  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ํด๋ž˜์Šค๋“ค์ด๋‹ค. ๋Œ€ํ‘œ์ ์œผ๋กœ Service, Repository, Factory, Helper, Middleware๊ฐ€ ์žˆ๋‹ค. ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†๋Š” ๋ณต์žกํ•œ ์ž‘์—…์„ Provider๋“ค์—๊ฒŒ ๋„˜๊ฒจ์ฃผ๋Š” ์˜ˆ์‹œ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ๋‹ค.

Inversion of Control

https://en.wikipedia.org/wiki/Inversion_of_control

์›๋ž˜๋Š” ์œ ์ €๊ฐ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฝ”๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์ง€๋งŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฝ”๋“œ๊ฐ€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ ์ € ์ฝ”๋“œ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“œ๋Š” ํŒจํ„ด์„ IoC๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

Dependency Injection์ด๋ผ๊ณ , ์ƒ์„ฑ์ž ํƒ€์ž„์— ์˜์กด์„ฑ Provider๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ๊ณผ์ •์„ ์˜๋ฏธ. ์•„๋ž˜์˜ ์ฝ”๋“œ์˜ Provider์ธ CatsService๋Š” @Injectable ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋‹ฌ๊ณ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ปจํŠธ๋กค๋Ÿฌ์˜ ์ƒ์„ฑํƒ€์ž„์— ์ฃผ์ž…๋  ์ˆ˜ ์žˆ๋‹ค.

// interfaces/cats.interface.ts
export interface Cat {
    name: string;
    age: number;
    breed: string;
}

// cats/cats.service.ts
@Injectable()
export class CatsService {
    private readonly cats: Cat[] = [];
    create(cat: Cat) {
        this.cats.push(cat);
    }
    findall(): Cat[] {
        return this.cats
    }
}

// cats/cats.controller.ts
@Controller('cats')
export class CatsController {
    /**
     * ์ž๋™์œผ๋กœ ์ฃผ์ž…๋˜๋Š” CatsService
     */
    constructor(private catsService: CatsService) {}

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

    @Get()
    async findAll(): Promise<Cat[]> {
        return this.catsService.findAll();
    }
}

์ƒ์„ฑ์ž์— ์˜์กด์„ฑ์„ ๋„ฃ๊ณ ์‹ถ์ง€ ์•Š๋‹ค๋ฉด Property-based injection์„ ์ฐธ๊ณ .

Modules#

๊ฐ๊ฐ์˜ ๋„๋ฉ”์ธ๋“ค (users, cats, posts, comments, ...)์€ ๊ฐ๊ฐ์˜ app์œผ๋กœ ๊ด€๋ฆฌ๋œ๋‹ค. ์ด๋•Œ app์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์ฝ”๋“œ๋“ค์˜ ์ง‘ํ•ฉ์„ ๋ชจ๋“ˆ์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค. nest g module <module-name> CLI ๋ช…๋ น์–ด๋กœ ์ƒˆ ๋ชจ๋“ˆ ์ƒ์„ฑ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฃจํŠธ๋ชจ๋“ˆ์— import๋ฅผ ํ•ด์•ผํ•œ๋‹ค.

exports ์†์„ฑ์œผ๋กœ ๋ชจ๋“ˆ ๋‚ด provider๋ฅผ ๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ ๋‹ค.

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

@Global ์–ด๋…ธํ…Œ์ด์…˜ ๋‹ฌ๋ฉด providers๋ฅผ ๋‹ค๋ฅธ ๋ชจ๋“  ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

Middleware#

express.js์˜ ๋ฏธ๋“ค์›จ์–ด์˜ ์Šคํ‚ด์„ ๊ณ ์Šค๋ž€ํžˆ ๋”ฐ๋ผ๊ฐ€๊ณ  ์žˆ๋‹ค. ๋‹จ์ˆœ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ next() ๋ฅผ ์‚ฌ์šฉ, ๋‹ค์Œ ๋ฏธ๋“ค์›จ์–ด๋กœ ์š”์ฒญ์„ ์ „๋‹ฌํ•  ์ˆ˜๋„ ์žˆ์œผ๋ฉฐ, NestMiddleware ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปจํŠธ๋กค๋Ÿฌ์— ์˜์กด์„ฑ ์ฃผ์ž…์„ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

// logger.middleware.ts
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        console.log('reqest...');
        next();
    }
}

// app.module.ts
@Module({
    imports: [CatsModule],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(LoogerMiddleware)
            .forRoutes('cats');
    }
}

consumer.forRoutes ์˜ ์ธ์ž๋กœ ์ข€ ๋” ๊ตฌ์ฒด์ ์ธ path, method, ์‹ฌ์ง€์–ด๋Š” controller๋ฅผ ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

Exception Filters (์ดํ•ด๋ถ€์กฑ)#

Nest์—๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์˜ˆ์™ธ ๊ณ„์ธต์ด ๋‚ด์žฅ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ์™ธ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ์—์„œ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์œผ๋ฉด ์ด ๊ณ„์ธต์—์„œ ์˜ˆ์™ธ๋ฅผ ํฌ์ฐฉํ•˜์—ฌ ์ ์ ˆํ•œ ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์‘๋‹ต์„ ์ž๋™์œผ๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

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

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}
  • getRequest<Request>์™€ getResponse<Response>๋ฅผ ์‚ฌ์šฉ, ์˜ˆ์™ธ ์•ˆ์—์„œ๋„ ์š”์ฒญ๊ณผ ์‘๋‹ต ๊ฐ์ฒด์˜ ์ฐธ์กฐ๋ฅผ ์–ป์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
  • exception์€ ํ˜„์žฌ ์ฒ˜๋ฆฌ์ค‘์ธ ์˜ˆ์™ธ๊ฐ์ฒด
  • host๋Š” ArgumentHost๊ฐ์ฒด๋กœ, ์•„์ง ์ž˜ ๋ชจ๋ฅด๊ฒƒ๋‹ค

    • HTTP ์˜ˆ์™ธ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ (MSA, WebSocket)์— ๋Œ€์‘ํ•˜๊ธฐ ์œ„ํ•ด ArgumentHost๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์ ๋งŒํผ์€ ์•Œ๊ณ  ๋„˜์–ด๊ฐ€์ž
  • ํ•„ํ„ฐ ์—ฐ๊ฒฐ๋ฒ•: ์ปจํŠธ๋กค๋Ÿฌ (ํ˜น์€ ๋ฉ”์„œ๋“œ)(ํ˜น์€ bootstrap ์•ˆ์—)์˜ ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ @UseFilters()๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
    throw new ForbiddenException();
}

Pipes#

ํŒŒ์ดํ”„๋Š” ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๋ผ์šฐํŒ…์„ ํ•˜๊ธฐ ์ „ ๋‘ ๊ฐ€์ง€ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

  1. ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ (์˜ˆ์‹œ. string โŸถ number)
  2. ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ

๋ชจ๋“  PipeTransform ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  @Injectable() ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ฐ–๊ณ ์žˆ๋Š” ํด๋ž˜์Šค๋Š” Pipe์ด๋‹ค.

path variable์„ ์›ํ•˜๋Š” ํƒ€์ž…์œผ๋กœ ํŒŒ์‹ฑํ•ด์•ผํ•  ๊ฒฝ์šฐ, ์˜ˆ๋ฅผ ๋“ค์–ด ์ธ์ž์— ๋“ค์–ด์˜ฌ @Param์— ParseXXXPipe๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.catsService.findOne(id);
}

path variable๋งŒ ๋˜๋ƒ? query parameter๋„ ๋œ๋‹ค!

@Get()
async findOne(@Query('id'), ParseIntPipe) id: number) {
    ...
}

ํŒŒ์‹ฑ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ (์˜ˆ๋ฅผ ๋“ค์–ด GET cats/hello๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ) ์˜ˆ์™ธ๋ฅผ ๋˜์ง„๋‹ค. ๊ทธ ์˜ˆ์™ธ๋Š” ์œ„์—์„œ ์„ค๋ช…ํ•œ Exception Filter์—์„œ ์ฒ˜๋ฆฌ๋˜์–ด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์‘๋‹ตํ•˜๊ฒŒ ๋œ๋‹ค.

  • [?] ๊ทธ๋ž˜๋„ ๋ฉ”์„œ๋“œ ์„ ์–ธ ์ˆœ์„œ๊ฐ€ ๋ฌธ์ œ๊ฐ€ ๋  ๊ฒƒ ๊ฐ™์€๋ฐ? @Get('cat/:id')์™€ @Get('cat/breed') ์ด ์ˆœ์„œ๋กœ ๋ผ์šฐํŒ…์„ ์ฒ˜๋ฆฌํ•ด๋ฒ„๋ฆฌ๋ฉด ์ „๋ถ€ ์ฒซ๋ฒˆ์งธ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๋“ค์–ด๊ฐˆ ๊ฒƒ์ด๊ณ  ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค.

    // not ok
    @Get(':id')
    findOne(@Param('id', ParseIntPipe) id: number): string {
      return `meowingtone #${id}`;
    }
    
    @Get('breed')
    findBreed(): string {
      return 'you requested breed!';
    }
    
    // ok
    @Get('breed')
    findBreed(): string {
      return 'you requested breed!';
    }
    
    @Get(':id')
    findOne(@Param('id', ParseIntPipe) id: number): string {
      return `meowingtone #${id}`;
    }
    

๋งŒ์•ฝ์— ํŒŒ์ดํ”„๋ฅผ ํ†ตํ•ด์„œ ๋ณ€ํ™˜ํ•ด์•ผ ํ•˜๋Š” ํƒ€์ž…์ด ๊ธฐ๋ณธํƒ€์ž…์ด ์•„๋‹ˆ๋ผ๋ฉด? => DTO Validation์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. DTO Validation using class-validator {NestJS}

Guards#

guard๋Š” @Injectable ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ฐ€์ง€๊ณ  CanActivate ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

guards๋Š” express.js ๋ฏธ๋“ค์›จ์–ด์˜ ๋‹จ์ ์ธ next๊ฐ€ ๋ˆ„๊ตฌ์ธ์ง€ ๋ชจ๋ฅธ๋‹ค๋Š” ํŠน์ง•์„ ExecutionContext ์ธ์Šคํ„ด์Šค๋ฅผ ํ†ตํ•ด ๊ทน๋ณตํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ .

๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ธ ์‚ฌ์šฉ์‚ฌ๋ก€๋Š” ์—ญ์‹œ ์‚ฌ์šฉ์ž ์ธ๊ฐ€์ด๋‹ค. nestjs.com/security/authentication ์‚ฌ์šฉ์ž ์ธ๊ฐ€์— ๋Œ€ํ•œ ๋กœ์ง์„ ๋งŒ๋“ค์–ด ์ „์—ญ์ ์œผ๋กœ or ์ปจํŠธ๋กค๋Ÿฌ or ๋ฉ”์„œ๋“œ ์Šค์ฝ”ํ”„์—์„œ ๋™์ž‘ํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

ExecutionContext ๊ฐ์ฒด๋Š” ArgumentHost๋ฅผ ์ƒ์†ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— request๊ฐ์ฒด์™€ response ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    /**
     * ExecutionContext์—์„œ request ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค. 
     * ํ—ค๋”์— ๋‹ด๊ธด ํ† ํฐ์„ validateํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๊ฒฐ๊ณผ๋Š” ๋™๊ธฐ์ ์œผ๋กœ
     * (Promise), ํ˜น์€ ๋น„๋™๊ธฐ์ ์œผ๋กœ (Observable), ํ˜น์€ ๊ทธ ์ฆ‰์‹œ
     * (boolean) ๋ฆฌํ„ดํ•  ์ˆ˜ ์žˆ๋‹ค.
     */
    return validateRequest(request);
  }
}

User Role์— ๋”ฐ๋ผ์„œ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜๋„ ์žˆ๋‹ค. admin ์œ ์ €๋งŒ์ด ์ ‘์†ํ•  ์ˆ˜ ์žˆ๋Š” ๊ด€๋ฆฌํŽ˜์ด์ง€์— ๋Œ€ํ•ด์„œ ์ปค์Šคํ…€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์œ ์ €์˜ ์—ญํ• ์„ ๊ฑฐ๋ฅผ ์ˆ˜๋„ ์žˆ๋‹ค. Role-based authentication

Interceptors#

NestInterceptor ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  @Injectable ํ•œ ํด๋ž˜์Šค๋ฅผ ์ธํ„ฐ์…‰ํ„ฐ๋ผ๊ณ  ๋ถ€๋ฆ„. ExecutionContext(์ค„๊ธฐ์ฐจ๊ฒŒ ๋ดค๋˜๊ฑฐ) + CallHandler(handle์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ์ปจํŠธ๋กค๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ฐ„์ ‘ ํ˜ธ์ถœ(RxJS๋ฅผ ์‚ฌ์šฉํ•œ๋Œ„๋‹ค))๋ฅผ ํ†ตํ•˜์—ฌ intercept() ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.

CallHandler๋ฅผ ์•„์˜ˆ ๊ฐ์‹ผ ๊ฑธ ๋ณด๋ฉด request ~ response ๋กœ์ง๋ณด๋‹ค ์Šค์ฝ”ํ”„๊ฐ€ ๋„“๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle() /** 
                 * handle์„ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์œผ๋กœ request-response 
                 * handler๊ฐ€ ์‹คํ–‰๋œ๋‹ค. ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋ญ˜ ์ŠคํŠธ๋ฆผ์— ๋„ฃ๋Š”์ง€๋Š”
                 * ์ž˜ ๋ชจ๋ฅด๊ฒ ๋‹ค.
                 */
      .pipe(
        /**
         * `tap`์€ observable stream์ด ์ •์ƒ์ ์œผ๋กœ (์˜ˆ์™ธ๋„ ํฌํ•จ) 
         * ์ข…๋ฃŒ๋์„๋•Œ ๋ฐ˜์‘ํ•œ๋‹ค. 
         */
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}
  • Binding Interceptors with @UseInterceptors(LoggingInterceptor) for controller class & method
    • ์ „์—ญ์ ์œผ๋กœ ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๋ ค๋ฉด useGlobalInterceptors()๋ฅผ ๋ถ€ํŠธ์ŠคํŠธ๋žฉ ๋‹จ๊ณ„์— ๋„ฃ์–ด์ฃผ์ž.
  • Response Mapping
    • handle()์ด #RxJS์˜ Observable ๊ฐ์ฒด๋ผ๋Š” ์‚ฌ์‹ค์„ ์•Œ์•˜์œผ๋‹ˆ ์ด ์ŠคํŠธ๋ฆผ์œผ๋กœ๋ถ€ํ„ฐ ๋ฌด์–ธ๊ฐ€๋ฅผ mapํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ์™„์ „ ํ•จ์ˆ˜ํ˜•์ธ๋ฐ?
    • RxJS๋ฅผ ์ข€ ๋ฐฐ์›Œ์•ผ ์ดํ•ด๊ฐ€ ๋  ๋“ฏ ํ•œ๋ฐ, ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์“ธ์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค.

[TODO] JEST#

nest ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ๋‚˜์„œ ๋”ฑ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ์ธ ์ƒํƒœ์—์„œ jest๋ฅผ ๋Œ๋ ค๋ณด์ž. ๋ฐ”๋กœ ์‹คํŒจํ•œ๋‹ค. getHello ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ๊ฐ€ ์—†๋‹ค๊ณ  ๋‚˜์˜ค๋Š”๋ฐ, ๋ณ„๋„๋กœ ์ธ์ ์…˜์„ ํ•ด์•ผํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

TestRun "all-gifts:watch-tests-0:process-start:0 (0)" started

> all-gifts@0.0.1 test
> jest --testLocationInResults --json --useStderr --outputFile /tmp/jest_runner_all_gifts_1000.json --watch --no-coverage --reporters default --reporters /home/choiwheatley/.vscode-server/extensions/orta.vscode-jest-6.2.2/out/reporter.js --colors

 FAIL  src/app.controller.spec.ts
  โ— Test suite failed to run

    src/app.controller.spec.ts:19:28 - error TS2339: Property 'getHello' does not exist on type 'AppController'.

    19       expect(appController.getHello()).toBe('Hello World!');
                                  ~~~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.91 s

Custom decorators#

Decorator - {nestjs}

JS async, await, promise#

https://springfall.cc/article/2022-11/easy-promise-async-await

OS๋ ˆ๋ฒจ์—์„œ ์Šค์ผ€์ค„๋ง์˜ ๋Œ€์ƒ์€ ํ”„๋กœ์„ธ์Šค์ด์ง€๋งŒ JS ๋ ˆ๋ฒจ์—์„œ ์Šค์ผ€์ค„๋ง์˜ ๋Œ€์ƒ์€ ํ•จ์ˆ˜์ด๋‹ค.

๋”ฐ๋ผ์„œ, ์ฝœ๋ฐฑํ•จ์ˆ˜๋Š”, ๋‚ด๊ฐ€ ํ•จ์ˆ˜๋ฅผ ์›ํ•  ๋•Œ ํ˜ธ์ถœํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์ˆ˜๋‹จ์œผ๋กœ ๋‚˜์˜จ ๊ฒƒ์ด๋‹ค.

JS์—์„œ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ฒƒ์€ ๋™์‹œ์— ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ๋™์‹œ์— ํ•˜์ง€ ๋ชปํ•œ๋‹ค.

tasks, microtasks, queues and schedules {js}

[TODO] RxJS#

Database typeorm#

postgresql#

Model-View-Controller ๋ฅผ ์‚ฌ์šฉํ•œ ํ’€์Šคํƒ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ#

HTML ํ…œํ”Œ๋ฆฟ ์—”์ง„ ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉ, app์— ์ •์ ํŒŒ์ผ์˜ ์œ„์น˜๋ฅผ ์„ค์ •ํ•˜๊ณ  view engine์„ ์„ธํŒ…ํ•ด์ค€๋‹ค.

Pagination#

Help Centre#

Packages#