[Nestjs] NestJS가 각광받고 있는 이유? / 파일 트리 구조 / 실제코드
Code/Nestjs

[Nestjs] NestJS가 각광받고 있는 이유? / 파일 트리 구조 / 실제코드

반응형


파이널 프로젝트에서 백엔드를 맡으면서 nestJS를 사용했는데, 골치아팠던 nestJS를 이해하게 되면서 핫할 수 밖에 없는 스택임을 느꼈고, 이를 토대로 기술 발표를 진행했던 적이 있다.

오늘은 nestJS가 핫한 이유, nestjs의 활용과 유용성에 대해 포스팅을 하려고 한다.

포스팅에 앞서 ppt 자료가 포함되었던 발표이기에 간단한 ppt와 함께 nestJS에 대한 설명을 진행할 예정이다.


nestjs에 대한 소개와, 사용한 기능 및 느낀 점 총 2가지 포인트로 설명을 할 것이다.

NestJS의 특징

NestJS 특징 1
NestJS 특징2

 

첫 번째, 안정성

NestJS는 물론 자바스크립트 js에서도 작동이 가능하지만, nestJS 개발 당시 타입스크립트를 고려하여 제작되었기 때문에 타입스크립트를 적극적으로 지원한다.
타입스크립트를 지원한다는 것은, 타입스크립트의 장점 또한 갖고 있다는 것으로 보면 된다.

타입스크립트는 타입을 지정하여 개발자 또는 시스템이 코드를 읽고 디버깅 하는 데에 자바스크립트의 몇십배는 더 편하게 만들어 준다고 생각하는데, 이를 서버 개발 시에도 적극 활용하여 발생 할 수 있는 이슈를 미연에 방지할 수 있다.

또한 nestJS 아키텍쳐 자체가 모듈 별로 감싸진 형태로 작성되어 있기 때문에 nestJS 에서 지원하는 테스트를 직접 실행하며 정말 안정성있게 작업을 할 수 있다.

두 번째, 확장성

위에 말한 바와 같이 모듈식 아키텍쳐를 사용하고 있는데, 다른 라이브러리와 함께 사용할 수 있어 정말 유연한 확장성을 제공하고 있다.

세 번째, 캡슐화

객체지향프로그래밍 OOP의 특성 중 하나가 캡슐화이다.
NestJS는 비슷한 기능을 하는 컨트롤러, 서비스 등을 묶어 module 파일 내에서 모두 관리한다.
이처럼 간단하게 모두 분기시켜서 관리할 수 있다는 특징이 있다.

네 번째, 구조

node 개발자라면 한 번쯤 느껴봤을 법한 아키텍쳐의 불편함과 이슈.
nestjs는 개발자와 팀이 고도로 테스트 가능하고 확장 가능하며, 느슨하게 결합되어 있어 유지 관리가 아주 쉽다.
구조와 파일의 역할을 이해하고 익힌다면, 이렇게 편한 아키텍쳐를 가질 순 없다! 라고 현재까지 생각 중이다.

node 작업 시 파일 구성을 위한 디자인 패턴에도 여러가지 종류가 있겠지만 직접 진행하고 이용했던 것은 MVC 패턴이었다.
node의 경우에는 사용자가 파일 패턴 자체를 컨트롤 할 수 있는 이러한 자유가 있기때문에 좀 더 효율적인 패턴을 찾고, 사람들은 그것을 디자인 패턴으로 명명한다.

하지만, 이 자유로움에서 문제가 발생하는 경우가 있다.
프로젝트에 참여한 팀원이 많아지거나, 프로젝트의 규모가 클 수록, 개발자 개개인만의 아키텍쳐를 갖게되는 상황이 발생하게 되는데 이 경우 통일성을 지키기 어려워지고 유지 보수가 힘들어질 수 있다.

이를 보완한 게 NestJS이다.
nestJS는 컨트롤러, 모듈, 서비스 등 파일의 역할이 분명하기때문에 우선적으로 통일성이 있고 그로인한 구조화된 작업 진행이 가능하다.

사용한 기능 및 느낀 점

NestJS 구조

우선 파이널 프로젝트를 진행하는 4주 동안, nestJS의 공식문서가 책이었다면 닳았을 정도로 붙들고 있었다.
처음에는 구조 파악이 힘들었던 걸로 기억한다. 메인 파일이 있는 것도, 모듈도, 컨트롤러도, 서비스도 그래.. 다 알겠는데 실제로 어떻게 쓰는 건데? 라는 생각이 계속 있었다.

nestJS를 새로 공부하면서 신규 기술 스택을 공부하는 방법을 하나 깨달았는데,
각 파일들의 역할을 정확히 이해하고, 구조를 익히면 금방 익숙해진다는 사실을 알았다.

그래서 혹시라도 과거의 나와 같이.. 구조 파악에 고통받고 몸부림치며 발버둥 중인 분들이 계시다면 도움이 되었으면 좋겠어서 실제 코드를 예제로 들면서 설명을 해볼까 한다.

구조

Nestjs 공식문서

위에 구조들을 직접 이해하기 쉽게 말을 하자면,

Main.ts - (파일명 변경 불가) Nest 어플리캐이션 인스턴스를 생성하는 엔트리 파일, 기본적인 서버 실행, cors 옵션 등 서버 설정
xxx.module.ts - 어플리케이션 루트 모듈, 모듈 연결자 라고 생각, imports, controllers, service 등의 루트 연결 담당
xxx.controller.ts - 엔드포인트, 라우터 등을 분기
xxx.service.ts - 컨트롤러를 통해 작동되어야 하는 서비스 코드가 작성된 파일 (db 처리 진행)

위와 같이 크게 4가지로 볼 수 있다.

설명하기에 앞서 파일 트리 구조가 궁금한 분들이 있을 것 같아 첨부한다.


dist - 타입스크립트가 자바스크립트로 컴파일되어 저장되는 폴더
src - 리소스 폴더
entity - typeorm을 사용한 엔티티(스키마) 파일 저장소
dto - 엔티티의 타입을 지정하기 위해 만든 폴더 및 파일

dto의 개념은 nestjs와 typeorm을 같이 사용할 때 필요한 부분이라 일단은 설명 없이 넘어갈 예정이다.
위 구조에서 중요한 것은 엔드포인트 별로 src에서 폴더로 파일을 분기 한 것이다.

또 파일 중 .spec.ts 라고 되어있는 것들은 nestjs CLI 명령어를 통해 서비스, 모듈, 컨트롤러등을 생성했을 때 자동으로 추가되는 테스트 파일이다. 프로젝트 도중 직접 사용하진 않았다.

그리고 설명은 'group'을 예로 들며 설명을 진행할 것이다.

반응형



Main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as dotenv from 'dotenv';
import * as cookieParser from 'cookie-parser';

dotenv.config();

async function bootstrap() {
  const fs = require('fs');
  const keyFile = fs.readFileSync(__dirname + '/../../key.pem');
  const certFile = fs.readFileSync(__dirname + '/../../cert.pem');

  const app = await NestFactory.create(AppModule, {
     httpsOptions: {
       key: keyFile,
       cert: certFile,
     },
  });

  app.use(cookieParser());

  app.enableCors({
    origin: ['https://www.sounds-wave.com'],
    credentials: true,
    methods: ['GET', 'PUT', 'POST', 'PATCH', 'OPTIONS', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  });

  await app.listen(3000);
}
bootstrap();

NestJS 애플리케이션 인스턴스를 생성하기 위해 핵심 NestFactory클래스를 사용합니다.
NestFactory 응용 프로그램 인스턴스를 만들 수있는 몇 가지 정적 메서드가 있는데, 이 create()메서드는 INestApplication인터페이스 를 충족하는 응용 프로그램 개체를 반환합니다. - Nestjs 공식문서

핵심 기능 NestFactory을 사용하여 Nest 애플리케이션 인스턴스를 생성하는 애플리케이션의 엔트리 파일이다.
위와 같이, 프로젝트 내부에서 cors 옵션이나 http 리스너를 작동하는데 사용했다.

App.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FacebookStrategy } from './auth/facebook.strategy';
import { NoisesModule } from './noises/noises.module';
import { AuthController } from './auth/auth.controller';
import { UserService } from './user/user.service';
import { RecommendModule } from './recommend/recommend.module';
import { GroupsModule } from './groups/groups.module';
import { AuthService } from './auth/auth.service';
import { GoogleStrategy } from './auth/google.strategy';
import { User } from './entity/User.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot(),
    TypeOrmModule.forFeature([User]),
    NoisesModule,
    RecommendModule,
    GroupsModule,
  ],
  controllers: [AuthController],
  providers: [FacebookStrategy, GoogleStrategy, UserService, AuthService],
  exports: [TypeOrmModule],
})
export class AppModule {}


뭐가 많다고 당황할 필요 없다. 어디까지나 이미 구현 완료된 코드이다.
일단 제일 상위에 위치한 app.module.ts , 루트 모듈에 대해 설명을 진행하자면

nestJS에서 모듈은, 분기처리 되어있는 다른 모듈도 import를 통해 사용할 수 있다.
app.module에 모든 모듈을 연결했다.

controllers도 동일하게 app.module에서 사용하고 싶은 컨트롤러를 연결 하면 된다. 기본 셋팅은 app.controllers.ts로 되어있을텐데, 프로젝트 내부에서 우리는 app.module의 컨트롤러를 auth 인증 컨트롤러로 연결 시켰다.
providers는 이 모듈에 어떤 서비스를 사용할 것인지 알려주는 곳이다.
exports는 밖으로 내보낼 모듈의 이름또는 형식등을 기재하면 된다. 우린 typeorm 모듈로 전역에서 관리할 수 있게끔 진행했다.

그럼 app.module이 받아오고 있는 GroupsModule에 대해 추가 설명이 필요할 것 같다.

groups.module.ts

import { Module } from '@nestjs/common';
import { Group } from 'src/entity/Group.entity';
import { GroupsService } from './groups.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GroupsController } from './groups.controller';
import { Noise } from 'src/entity/Noise.entity';
import { Groupcomb_noise } from 'src/entity/Groupcomb_noise.entity';
import { Groupcomb_music } from 'src/entity/Groupcomb_music.entity';
import { Weather } from 'src/entity/Weather.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Group, Noise, Groupcomb_noise, Groupcomb_music, Weather])],
  controllers: [GroupsController],
  providers: [GroupsService],
  exports: [TypeOrmModule],
})
export class GroupsModule {}


imports - 타입오알엠 모듈 메소드를 사용하여, 필요한 엔티티(스키마)들을 받아오고 있다.
controllers - groups.controllers.ts 파일을 연결한다.
providers - groups.service.ts 파일을 연결한다.
exports - 모듈을 전역으로 내보낸다.

groups.controllers.ts

import { Controller, Post, Body, Request, Response, Get, Query, Param, Delete } from '@nestjs/common';
import { CreateGroupDto } from './dto/CreateGroupDto';
import { GroupsService } from './groups.service';

@Controller('groups')
export class GroupsController {
  constructor(private readonly groupsService: GroupsService) {}

  @Post()
  async findGroupcombNoiseData(@Body() group: CreateGroupDto, @Request() req, @Response() res) {
    const accessToken = await req.headers.authorization;
    const result = await this.groupsService.findGroupcombNoiseData(group, accessToken);

    if (result === "그룹 저장 성공!") {
    return res.status(200).send({ data : result})
    } else if (result === "이미 동일한 이름의 그룹이 존재합니다.") {
      res.status(400).send({data: result});
    } else if (result === "유효하지 않은 토큰입니다!") {
      res.status(401).send({data: result});
    }
  }

  @Get()
  async findAllGroups(@Request() req, @Query() query, @Response() res) {
    const accessToken = await req.headers.authorization;
    const userId: number = Number(query.userId);
    const data = await this.groupsService.findAllGroups(userId, accessToken);

    if (data === "유효하지 않은 토큰입니다!") {
      res.status(401).send({message : data});
    } else if (data === "저장된 그룹이 없는 유저!") {
      res.status(406).send({message : data });
    }
    else {
      res.status(200).send({ data, message: "해당 유저의 그룹을 성공적으로 불러왔습니다!" })
    }
  }

  @Delete('/delete/:id')
  async deleteGroups(@Param('id') param, @Request() req, @Response() res) {
    const accessToken = req.headers.authorization;
    const groupId = Number(param);

    const data = await this.groupsService.deleteGroupRequest(groupId, accessToken);

    if (data === "유효하지 않은 토큰입니다!") {
      res.status(401).send({ message : data });
    } ...
  }
}


상단 @Controller() 안에는 엔드포인트가 들어간다.
Class의 constructor에 읽기전용으로 groupsService를 상속받아 연결한다.
위의 예시와 같이 post, get, delete, patch 등 http 메소드를 nestJS에서는 데코레이터라는 명칭으로 부르곤 한다.

연결된 서비스에서 메소드를 실행한 후에 받환받은 값을 필터링하거나 처리하여 응답 처리를 진행하면 된다.

groups.service.ts


import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { createQueryBuilder, getConnection, getManager, Repository } from 'typeorm';
import { GroupcombNoiseIdDto } from './dto/groupcombNoiseIdDto';
import { CreateGroupDto } from './dto/CreateGroupDto';
import { Group } from 'src/entity/Group.entity';
import { Noise } from 'src/entity/Noise.entity';
import { Groupcomb_noise } from 'src/entity/Groupcomb_noise.entity';
import { Groupcomb_music } from 'src/entity/Groupcomb_music.entity';
import { Music_volume } from 'src/entity/Music_volume.entity';
import { Noise_volume } from 'src/entity/Noise_volume.entity';
import { User } from 'src/entity/User.entity';
import { Weather } from './../entity/Weather.entity';

@Injectable()
export class GroupsService {
  constructor(
    @InjectRepository(Group)
    private groupRepository: Repository<Group>,

    @InjectRepository(Noise)
    private noiseRepository: Repository<Noise>,

    @InjectRepository(Groupcomb_noise)
    private groupcombNoiseRepository: Repository<Groupcomb_noise>,

    @InjectRepository(Groupcomb_music)
    private groupcombMusicRepository: Repository<Groupcomb_music>,

    @InjectRepository(Weather)
    private weatherRepository: Repository<Weather>,
  ) {}
  
   async findGroupcombNoiseData(group: CreateGroupDto, token: string | undefined): Promise<string> {
    let noiseNames = [];

    if (!token) {
      return "유효하지 않은 토큰입니다!"
    }

    const checkSameGroupname = await this.findUserGroupName(group.groupName, group.userId);
    
    if (checkSameGroupname.length) {
      return "이미 동일한 이름의 그룹이 존재합니다."
    }

    await group.noises.forEach((noise: any) => {
      noiseNames.push(noise.name);
    });
    
    // noise 배열의 name으로 db내 noiseId를 찾는다
    const findNoiseId: any[] = await this.noiseRepository
    .createQueryBuilder('noise')
    .select("noise.id")
    .where("noise.name IN (:...name)", { name: noiseNames })
    .getMany()
    
    ....
    return "그룹 저장 성공!";
   }
  
   findGroupcombNoiseId = async (noises: any[], id: string) => {
    let checked: boolean = false;
    let groupcombId: number = 0;

    await noises.forEach((obj: any) => {
      for (let key in obj) {
        if (key !== "groupcombNoise_groupcombId" && obj[key] === id) {
          checked = true;
          groupcombId = obj.groupcombNoise_groupcombId;
        }
      }
    })
    return {checked, groupcombId};
  }

그룹 서비스 파일에서는 컨트롤러에서 호출된 메소드가 실행된다. 위와 같이 비동기처리 함수 findGroupcombNoiseData 메소드가 실행되면서 적당한 처리를 진행해주면 된다.

constructor에서 @InjectRepository(엔티티) 형식으로 작성하여 해당 엔티티 저장소에 접근(db 접근이라 생각하면 됨) 한다.
위에서 말한대로 typeorm으로 db 관리를 진행했기때문에 typeorm을 사용한다면 위와 같은 형식을 참고하여 진행하면 될 것 같다.


느낀 점
직접 nestJS를 사용해보면 알겠지만, 기본 구조 자체가 통일화되어있기 때문에 구조 파악이 명확하고 간편하다.
개인적으로 프로젝트를 하면서, 느꼈던 점이 아무래도 같은 프로젝트여도 담당 파트가 다르면 본인의 태스크가 아닌 경우 다른 파트 코드를 보는 것이 시간이 좀 걸리거나 읽기가 어려운 부분이 생길 수 있다.
근데 이번 프로젝트에서 같이 백엔드를 맡았던 팀원 분과 서로 코드를 보았을 때 빠르게 이해할 수 있었고, 디버깅 시 다른 파트에서 문제가 발생했을 때에도 쉽게 찾아낼 수 있었다.

타입스크립트, 타입오알엠을 사용하는 프로젝트를 진행할 예정이라면, nestJS를 꼭 한 번쯤은 사용해보면 좋지 않을까 한다.
실제로 기술을 익히는 데에 일주일 가까이 시간이 소요됐지만, 계속 익히고 구조를 파악하면서 효율성이 극대화 되었던 경험이 있다.
만약, 이후에 사이드 프로젝트, 개인 프로젝트를 진행하면서 서버 코드를 작성하게 된다면 무조건 nestJS를 사용하여 구현할 것 같다.


























반응형