[NestJS 강의] Slack 클론 코딩[백엔드 with NestJS + TypeORM] 섹션1

강경서·2025년 8월 25일
post-thumbnail

🐈 NestJS

Express 를 사용하여 Node.js 기반의 서버를 빠르게 구현할 수 있지만 NestJS 를 사용한다면 모듈 단위로 기능을 구분하여 구조화된 서버를 구현할 수 있습니다. 기본적으로 TypeScript 기반이며 ExpressFastify 위에서 동작하여 기반이 되는 라이브러리의 기능을 사용할 수 있습니다.

Setting

# NestJs Command Line Interface

npm install -g @nestjs/cli

# Creat NestJs Project

nest new folder-name 

전역으로 NestJS CLI 를 설치하여 NestJS 프로젝트를 만들 수 있습니다.
더불어 개발환경에서의 핫 리로딩의 설정 또한 가능한데 현재는 별도의 설정 없이도 해당 기능이 가능합니다.


Controller

controllerRouter역활을 수행합니다.

// src/app.controller.ts

import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('a')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('b') // GET /a/b
  getHello() {
    return this.appService.getHello();
  }
  
  @Post('b') // POST /a/b
  getHello() {
    return this.appService.postHello();
  }
}

@Controller @Get @Post 와 같은 데코레이터를 사용하여 간편하게 구현할 수 있습니다. 해당 데코레이터NestJS가 확인하여 함수의 기능을 확장합니다.

또한 @Controller @Get @Post인수값을 통해 라우터 경로를 설정가능합니다.


서비스를 분리하는 이유, 컨트롤러 장점

// Express

app.get('/', (req, res) => {
  res.send('Hello World!');
});
// NestJs
// src/app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello(); 
  }
}

// src/app.service.ts

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

위와 같이 NestJS 는 서비스를 분리하여 관리합니다. Express 에서는 하나의 라우터 및 미들웨어 내에서 요청, 비지니스, 응답 과정을 한번에 적어두는 경우가 많았는데, 서비스는 위의 비지니스 과정을 분리한 것입니다.

서비스트랜젝션 단위로 나누어 관리합니다.
트랜젝션 : 데이터베이스 등에서 하나의 논리적 업무를 더 이상 나눌 수 없는 최소한의 작업 묶음

이로 인하여 서비스의 재사용성이 높아지고 Express 와 달리 req res 에 대한 의존성이 없어 테스트하기에 편리합니다. (mocking 함수가 필요하지 않습니다.)

또한 공통 반환 타입( 예. API 응답 { code: ‘STATUS’, data: data } )을 Interceptor 를 사용하여 관리할 수 있습니다.


ConfigModule

NestJS여러 기능을 패키지로 다운 받아 사용이 가능합니다.

# NestJs Configuration

npm i --save @nestjs/config
// src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
})
export class AppModule {}

// src/main.ts

const port = process.env.PORT || 3000

위의 패키지는 .env.[name] 과 같은 파일 추가해 환경변수별로 관리할 수도 있습니다.

// src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
  controllers: [AppController],
  providers: [AppService, ConfigService],
})
export class AppModule {}

// src/app.service.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppService {
  constructor( private readonly ConfigService:ConfigService) {}

  getHello() {
    return this.ConfigService.get("SECRET")
  }
}

ConfigService 를 사용하면 환경변수를 NestJs 의 모듈이 관리할 수 있습니다.

// 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 './middleware/logger.middleware';

const getLoad = async () => {
  const response = await axios.get("/비밀키요청");
  return response.data;
};

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true, load: [getLoad] })],
  controllers: [AppController],
  providers: [AppService, ConfigService],
})
export class AppModule {}

실제 서비스에서 환경 변수를 비동기로 외부 불러와야하는 경우에도 forRootload 옵션에 함수를 넣어 사용 가능합니다.


LoggerMiddleware

NestJs 에서 로거를 구현 시에 pino 와 같은 라이브러리를 사용할 수도 있지만 미들웨어로 직접 만들어서 구현도 가능합니다.

// src/middlewares/logger.middleware.ts

import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private logger = new Logger('HTTP'); // 로그간의 구별을 위한 context 

  use(req: Request, res: Response, next: NextFunction) {
    const { ip, method, originalUrl } = req;
    const userAgent = req.get('user-agent') || '';

    // 해당 미들웨어는 라우터보다 먼저 실행되므로 비동기로 실행
    res.on('finish', () => {
      const { statusCode } = res;
      const contentLength = res.get('content-length');
   
      this.logger.log(
        `[${method}] ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
      );
    });

    next();
  }
}

// app.module.ts

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*'); // 미들웨어 등록
  }
}

implements, injectable(DI)

implements 는 부모 클레스를 상속하는 extends 와 달리 부모 클레스를 속성과 메서드를 반드시 다시 구현해 주어야 합니다. 이는 개발 시 오류를 줄여줄 수 있습니다.

// DI X
class UserService {
  private repo = new UserRepository(); // 직접 생성 (강결합)
}

// DI O
class UserService {
  constructor(private readonly repo: UserRepository) {} // 외부에서 주입 (약결합)
}

NextJsproviders 에 연결되어 있는 것들을 보고 DI(의존성 주입)을 해줍니다. 또한 NestJs 에서 클래스를 Provider 로 등록하려면 @Injectable() 을 붙입니다. 이러한 DI는 객체가 직접 의존하는 객체를 생성하지 않고, 외부에서 주입 받도록 하는 패턴으로 결합도를 낮추고 (코드 재사용 및 테스트 용이) 관리 편의성이 증가합니다.

// 직접 주입
providers: [AppService]

// 객체 기반 주입
providers: [
    {
      provide: AppService, // key
      useClass: AppService
    },
  ],

Provider 등록 시 클래스와 같은 경우 해당 클래스를 직접 넣어줄 수 도 있고, key 값인 provideuseClass useValue useFactory 와 같은 설정이 가능합니다.


📝 후기

이전에 Express를 한번 사용해본 경험이 있어 강의를 잘 이해할 수 있었습니다. 프론트가 아닌 백엔드에 대한 학습은 많이 해보지 않아 이번 학습이 좋은 기회라고 생각합니다. Express와 NestJS의 차이점을 중점으로 학습해두면 두 프레임워크에 대한 학습이 될 것 같습니다.


🧾 Reference

profile
기록하고 배우고 시도하고

0개의 댓글