TIL

23/12/22 TIL __ nest js 강의 정리.

GABOJOK 2023. 12. 22. 23:56

정의

효율적이고 확장 가능한 서버 에플리 케이션을 만들기 위한 프레임 워크

express를 기반으로 만들어 졌다.
ts로 이루어져 있다.
아키텍처의 주요 문제를 효과적으로 해결하지 못해서, 나왔고,
아키텍쳐를 제공하는데 이거 엥귤러에서 영향 받음.
이거 쓰면 고도의 테스트 가능, 확장 가능,

설치

//초기 셋팅 _ 글로벌로 설치_
npm i -g @nestjs/cli

//파일 생성. 시작단계. 대부분 npm사용함. __ 이 명령어 하나로 많은 파일들이 생성되어 편리.
nest new  프로젝트이름 

게시글 프로젝트.

필요한 모듈
    전체 모듈인 appModule(root)가 필요
     게시글 관련 모듈. // 공개글인지 비공개글인지 boardModel에서 정의
     인증 모듈

nestJS 기본 구조

  • eslintrc.js
    • 개발자들이 특정한 규칙을 가지고 코드를 깔끔하게 짤 수 있게 도와주는 라이브러리.
    • 타입스크립트를 쓰는 가이드 라인을 제시한다.
    • 문법에 오류가 나면 알려주는 등 유용한 역할을 한다.
  • prettierrc
    • 코드의 형식을 서로 맞추는데에 사용한다.
    • 작은따옴표를 사용할지, 큰 따옴표를 사용할지 등등
  • nest-cli.json
    • nest 프로젝트를 위해 특정한 설정을 할 수 있는 json 파일.
{
    "$schema": "https://json.schemastore.org/nest-cli",
    "collection": "@nestjs/schematics",
    "sourceRoot": "src",  // 프로젝트 로직파일 어디에서 작업할건지 설정.
    "compilerOptions": {
        "deleteOutDir": true
        }
}

시작점 파일이 있음
이걸 엔트리 포인트라고도 하고.
현재 우리는 main.ts가 시작점 파일

 

처리 과정

  1. 클라이언트가 요청을 보냄.
  2. req 객체에 담겨져 시작점 파일을 거쳐 컨트롤러로 전송됨.
  3. 컨트롤러에는 @Get( ) 과 같은 메소드들이 받아줌 요청을
  4. 근데 이 컨트롤러에서는 서비스단을 호출하면서 리턴함.
  5. 그럼 이제 서비스단 로직이 실행
  6. 서비스단 로직 결과값이 다시 컨트롤러에 전송
  7. 이걸 다시 컨트롤러는 res객체에 담아서 시작점 파일을 거쳐 클라이언트에게 전송해준다.

nestJS도 express랑 똑같다!!

진입점

express _ app.js 파일

app.use('/api/board', boardRouter);
app.use('/api/user', userRouter);

nest _ app.module.ts 파일

@Module({
    imports: [
        TypeOrModule.forRoot(typeORMConfig),
        BoardsModule,
        AuthModule,
        UserModule,
    ],
})
export class APPModule {}

요청 경로에 따른 각 라우터로 전송

express _ routes/user.route.js

router.get("/", userController.getUsers);
router.get("/:bandId", userController.getUser);

nest _ board.controller.ts

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

//컨트롤러단. 브라우저에서 유저가 요청을 보내면 3계층 중 먼저 여길 거친다.
@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    //이런식으로 요청.
    @Get() //@Get("/")과 같다.
    getHello(): string {
        return this.appService.getHello();
    }
}

서비스단

express _ controllers/users.js

export const getUsers = async (req, res)=>{
    let users = db.getUsers();
    return users 
}

nest _ board.service.ts

async getAllBoards (
    user: User,
): Promise<Board[]> {
    const query = this.boardRepositofy.createQueryBuilder("board");
    query.where("board.userId = :userId", {userId: user.id});
    const boards = await query.getMany();
    return boards;
}

 

 

 


 

 

nest JS 모듈

모듈이란 ?

  • @ 데코레이터로 주석이 달린 클래스 이다.
  • 각 응용 프로그램에는 하나 이상의 모듈 이 필요하다.
  • 어떠한 nestJS 어플리케이션 이던 AppModule이 1개는 필요하다.
  • 모듈은 관련된것 끼리 뭉쳐놓는다. (기능별)
  • 모듈은 기본적으로 싱글 톤 이다.
    • 따라서 여러 모듈간 쉽게 공급자의 동일한 인스턴스를 공유할 수 있다.
    • 예를 들면 하나의 공용 모듈을 두고, userModule, orderModule 에서 사용하듯 말이다.

모듈 생성하는 명령어

nest g module boards

명령어를 분석해 보면

  • nset _ nestCli 사용하겠다는 의미
  • g _ generate 하겠다는 의미
  • module _ 내가 만들고 싶은 스케마 . 모듈을 생성하겠다. 라는말.
  • boards _ 만들려고 하는 스케마의 이름

여기서 nest의 편리한 부분이 나온다. 저렇게만 입력하면, 알아서 다른 파일들에서도 임포트를 해온다.

controller

  • @Controller 데코레이터로 클래스를 데코레이션 해서 정의된다.
@Controller({})
export class BoardController {}

 

 

 

Handler

  • 핸들러는 @Get, @Post, @Delete같은 데코레이터로 장식된
  • 컨트롤러 클래스 내의 메소드이다.

 

Controller 생성하는 명령어

nest g controller boards --no-spec 

명령어를 분석해 보면

  • nset _ nestCli 사용하겠다는 의미
  • g _ generate 하겠다는 의미
  • module _ 내가 만들고 싶은 스케마 . 모듈을 생성하겠다. 라는말.
  • boards _ 만들려고 하는 스케마의 이름
  • --no-spec _ 테스트를 위한 소스코드 생성 하지 않겠다.

이렇게 입력을 하게 되면, 컴퓨터는 이렇게 일하기 시작한다.

  1. cli가 boards 폴더를 찾는다.
  2. boards 폴더 안에 controller 파일을 생성한다.
  3. boards 폴더 안에 module 파일을 찾는다.
  4. module 파일에 controller 넣어준다.

 

 

Providers 란?

  • nest의 기본 개념
  • ==종속성으로 주입 할 수 있다는 특징이 있다. ==
  • 즉 객체는 서로 다양한 관계를 만들 수 있으며, 객체의 인스턴스를 "연결" 하는 기능은 대부분 Nest 런타임 시스템에 위임될 수 있다.
  • 대부분의 nest 기본 클래스들은 프로바이더로 취급 될 수 있다.
  • (서비스, 리포지토리, 팩토리, 헬퍼 등 )

한마디로 컨트롤러에서 다 구현하지 않고, 서비스 단 혹은 펙토리, 혹은 헬퍼 등에서 필요한 로직을 구현하는데, ==컨트롤러에서 사용할 수 있게 객체를 넣어주는걸 말함.

따라서 서비스도 repository 도, 팩토리도 provider 라고 볼 수 있다.

 

 

 

Service

  • @ Injectable 이라는 데코레이터로 감싸져서 모듈에 제공된다.
  • (injectable _ 주입가능한)
  • 이 서비스 인스턴스는 애플리케이션 전체에서 사용 될 수 있다.
  • 아까 서비스는 프로바이더에 해당한다고 했잖음?
  • 컨트롤러 단에 서비스를 주입할 수 있어야 함.
  • 이걸 종속성 주입(디펜던씨 인젝션) 이라고도 하는디 아래 간단한 예를 봐보자
  • export class BoardsController { //이런식으로 컨스트럭터 파라미터 자리에 서비스단을 타입으로 지정해준다. constructor (private boardsService: BoardsService){} @Get('/:id') getBoardById(@Param("id") id: string): Board { //그리고 이런식으로 리턴값에 서비스단을 호출한다. return this.boardsService.getBoardById(id); } }

원래 주입을 이런식으로 했었다 typeScript에서 말이다.

@Controller("boards")
export class BoardsController {
    boardsService: BoardsService;  // 프로퍼티 정의 및 타입 지정

    constructor(boardsService: BoardsService){
        this.boardsService = boardsService // 프로퍼티 정의 및 타입 지정.
        // 이 클래스 내의 boardsService에 파라미터값을 넣어준다는 의미이다.
    }
    @Get()
    getAllTask(): Board[] {
        return this.boardsService.getAllBoards();
    }

}

그런데
접근 제한자를 생성자 안에서 선언하면,
==접근제한자가 사용된 생성자 파라미터는 암묵적으로 클래스 프로퍼티로 선언되기 때문이다.

@Controller("boards")  //여기 있는건 경로임.
export class BoardsController {
    constructor(private boardsService: BoardsService){
        getAllTask(){
            this.boardsService
        }
    }
}

//private 접근 제어자를 사용함으로 다른 곳에서 접근할수없도록.

 

 

service를 만드는 명령어

nest g service boards --no-spec

Provider 등록하기

  • provider를 사용하려면 Nest에 등록을 해야함. (boards.module.ts 파일에서 등록 가능.)
  • 아래처럼 module 데코레이션 안에 지정을 해주면 된다.
    @Module({
    controllers: [BoardsController],
    //provider 할 모듈을 적어준다.
    prociders: [BoardsService]
    })
    export class BoardsModule {}

service단 살펴보기

import { Injectable } from '@nestjs/common';
@Injectable() //이 데코레이터 덕분에 다른 컴포넌트에서 이 서비스 사용 가능해짐
export class BoardsService {}

전체적인 흐름 파악하기

import { Controller, Get } from '@nestjs/common';
import { BoardsService } from './boards.service';


@Controller('boards') // 여기 있는건 경로이다. 
export class BoardsController {
    // boardsService: BoardsService; // 프로퍼티 정의 및 타입 지정
    constructor(private boardsService: BoardsService) {
        // this.boardsService = boardsService; // 프로퍼티 정의 및 타입 지정.
        // 이 클래스 내의 boardsService에 파라미터값을 넣어준다는 의미이다.
    }

    @Get(). //@Get('/')와 동일
    getAllBoard() {
        return this.boardsService.getAllBoards();
    }
}
  1. @Controller('boards') 의 경로인 localhost:3000/boards 로 접속하면
  2. 아래 BoardsController 이 클래스 구동.
  3. 거기서 @Get('/')에 접속을 한것이니 바로 아래에 있는 getAllBoard 가 구동.

즉 정리하자면
클라이언트에서 요청을 보내면, 먼저 컨트롤러로 가며, 컨트롤러에서 알맍은 요청 경로에 라우팅 해서 해당 핸들러로 가게 된다.
그런 뒤에 요청 처리를 위해 서비스로 들어가고, 그에 해당하는 로직을 서비스에서 처리해준 뒤에
컨트롤러에 리턴값을 보내준 후 컨트롤러에서 클라이언트로 결과값을 보내준다.
그래서 컨트롤러에서 요청을 처리하고 결과를 리턴해주는 역할을 한다.

 

 

 

 

 

게시글 서비스를 위한 board model 만들기


게시글 조회하기 기능

class를 이용하거나 interface를 이용해서 모델을 정의한다. 2가지중 한개로 정의한다.

Interface와 classes의 차이점.

  • interface는 변수의 타입만을 체크
  • classess는 변수의 타입도 체크하고, 인스턴스 또한 생성할 수 있다.

ID값 주기.
만약 디비를 안쓰고 만든다면 아이디 값을 주는게 문제인데
id값을 주려면 uuid 라는 모듈 을 이용해 임의로 만든다.
모듈 설치하면 되는데

npm install uuid --save
import { v1 as uuid } from 'uuid';

const board: Board = {
    id: uuid(),
    title,
    description,
    status: BoardStatus.PUBLIC,
};

그럼 이렇게 나옴
"id": "625de830-a0aa-11ee-bd01-9bbd65164009",


게시글 작성하기 기능

사용자가 입력한 정보 받아오기

  • 기존에 express환경에서는 req.body를 이용해서 사용자가 입력한 정보를 받아왔다.
  • nest 환경 에서는 @Body() body 를 이용해서 가져온다.
@Post() //핸들러 라고 부름
createBoard(@Body() body){
    console.log('body', body);
}
  • 만약 전체의 데이터가 아닌 title한개만 가지고 오고 싶다면
  • @Body('title') title 이런식으로 가져온다.

DTO (Data Transfer Object)

  • 계층간 데이터 교환을 위한 객체
  • DB에서 데이터를 얻어 Service나 Controller 등으로 보낼때 사용하는 객체
  • 데이터가 네트워크를 통해 전송되는 방법을 정의하는 객체
  • interface나 class를 이용해서 정의될 수 있다.
  • nestJs에서는 클래스를 이용하는걸 추천하고 있다.

DTO(Data Transfer Object) 를 사용하는 이유

  • 데이터 유효성을 체크하는데 효율적이다.
  • 더 안정적인 코드로 만들어 준다.
  • 타입스크립트의 타입으로 사용된다.
  • 실무에서는 주고받는 데이터가 한두개가 아니라 방대하기 때문에 수정하다보면 에러날 확률이 높다.
  • 그러다 보니 안정성을 위해 사용.
  • 갑자기 한곳에서 프로퍼티 이름을 바꿔줘야 할때 이런 문제가 있기때문에 안정적이라는 말.

DTO 파일 작성하기

  • 클래스를 이용해 작성하는 수업을 진행
  • 그 이유로는 인터페이스와 다르게 클래스는 런타임에서 작동하기 때문에 파이프 같은 기능을 이용할 때 더 유용하다.
export class CreateBoardDto {
    title: string;
    description: string;
}

특정 게시물 조회하기

DTO를 사용해서 데이터를 주고받을때 
@Body() body 로 이용했다면, params값은 이렇게 받는다.
@Get(':id')
getBoardById(@Param('id') id: string): Board {
    return this.boardsService.getBoardById(id);
}
  • 만약 여러개의 params 값을 가져와야 한다면 괄호 안에 아무 값도 없이
  • @Param() 으로 가져오면 된다.

파이프

파이프란?

  • @injectable( ) 데코레이터로 주석이 달린 클래스임.
  • Joi는 라이브러리였다면, pipe는 nest에 메커니즘임
  • 데이터 변환과, 유효성 검증을 위해 사용된다.
  • 파이프는 컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동한다.
  • Nest는 메소드가 호출되기 직전에 파이프를 삽입하고,
  • 파이프는 메소드로 향하는 인수를 수신. 그리고 이에 대해 작동한다.
  • 그러니까 원래는 파이프가 없다면 클라이언트 요청이 들어온 경우 바로 라우터 쪽으로 가는데,
  • 파이프가 있다면? 파이프를 먼저 거친다.

Data Transformation (데이터 변환)

  • 입력 데이터를 원하는 형식으로 변환한다.(예를들면 문자열에서 정수로.)
  • 만약 숫자를 받길 원하는데 문자열 형식으로 온다면, 파이프에서 자동으로 숫자로 바꿔준다.

Data Validation (데이터 유효성 검사)

즉 파이프는 변환과 유효성 검사 모두 해준다.
라우터 헨들러가 처리하는 인수에 대해 말이다.
또한 메소드 실행 직전에 작동해 메소드로 향하는 인수에 대한 문지기 역할을 한다.
(마치 미들웨어 같다)

Pipe 데이터 변환하는 기능 사용하기

    설치하기
npm i class-validator class-transformer --save

PIPE 사용하는 방법 (Binding Pipes)

  1. Handeler-level Pipes
  2. Parameter-level Pipes
  3. Global-level Pipes

Handeler-level Pipes

  • Handeler-level 에서는 @UsePipes() 데코레이터를 이용해서 사용한다.
  • 이 파이프는 위치한 헨들러 내에서만 작동이 된다.
  • 또한 모든 파라미터에 적용된다.
@Post()
@UsePipes(pipe)
createBoard(
    @Body('title') title,
    @Body('description') description
){

}

Parameter-lever Pipes

  • 특정한 파라미터 에게만 적용이 된다.
  • 아래의 경우 title만 파라미터 파이브가 적용된다.
  • ==@Body('title', ParameterPipe) title,==
@Post()
createBoard(
    @Body('title', ParameterPipe) title,
    @Body('description') description
){}

Global Pipes

  • 글로벌 파이프로써 애플리케이션 레벨의 파이프이다.
  • 클라이언트 에서 들어온 모든 요청에 적용이 된다.
  • 가장 상단 영역인 main.ts에 넣어 사용한다.
  • ==app.useGlobalPipes(GlobalPipes);==
async function bootstrap(){
    const app = await NestFactory.create(AppModule);
    app.useGlobalPipes(GlobalPipes);
    await app.listen(3000);
}
bootstrap()

Pipe는 어디서 가져와서 쓰나?

  • 직접 만들어서 쓰거나
  • Nest JS에서 만들어둔 파이프를 사용하거나.
    • ValidationPipe
    • ParseIntPipe
    • ParseBoolPipe
    • ParseArrayPipe
    • ParseUUIDPipe
    • DefaultValuePipe

예를들면 이런식으로 사용한다.
params 값으로 숫자를 받기를 예상하고 해두었는데,
만약 문자열이 왔다. ? ? 보통은 에러가 안난다.
그런데 이런 경우 처리를 해주는게 맞으니깐 처리를 해줘야 하는데,

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

이렇게 하면 에러가 난다.!!! 이런식으로 사용하는 구나 정도만 알고 넘어갈것.

파이프로 유효성 검사 하기.

    설치하기
npm i class-validator class-transformer --save

빈값 입력 막기

  • Dto 파일에 validator의 속성을 적어준다.
import { IsNotEmpty } from 'class-validator';

export class CreateBoardDto {
    @IsNotEmpty()
    title: string;
    @IsNotEmpty()
    description: string;
}
  • boards.controller.ts 파일에서도 validationPipe를 사용하겠다고 표시해 준다.
    @Post()
    @UsePipes(ValidationPipe)
    createBoard(
     @Body() createBoardDto: CreateBoardDto,
     ): Board {
         return this.boardsService.createBoard(createBoardDto);
     }

특정 게시물을 찾을때 없는 경우 결과값 처리.

  • 현재 특정 게시물을 id로 가져올때 없는 게시물을 가져오려 하는 경우 처리.
getBoardById(id: string): Board {
    const found = this.boards.find((board) => board.id === id);

    if (!found) {
        throw new NotFoundException(`${id}를 찾을 수 없습니다.`);
    }
    return found;
}

없는 게시물을 지우려 할때 처리

deleteBoard(id: string): void {
    const found = this.getBoardById(id);
    this.boards = this.boards.filter((board) => board.id !== found.id);
}

커스텀 파이프 구현하기

Pipe Transform.

  • 인터페이스 중 하나
  • 모든 파이프에서 구현해 주어야 하는 인터페이스 이다.
  • 모든 파이프는 transform() 메소드를 필요로 한다.
  • 인터페이스를 새롭게 만들 커스텀 파이프에 구현해야 한다.
export class BoardStatusValidationPipe implements PipeTransform{
    transform(value: any, metadata: ArgumentMetadata){
        console.log('value', value);
        console.log('metadata', metadata);

        return value;
    }
}

잠깐! extends와 implement의 차이

  • implements
    • implements 키워드는 클래스가 인터페이스를 구현(implement)한다는 것을 나타냅니다.
    • 클래스에서 인터페이스를 구현할 때 사용됩니다.
    • 인터페이스는 ==클래스가 가져야 할 속성과 메서드의 구조를 정의하며, 클래스는 이러한 구조를 따라야 합니다.==
    • 클래스가 구현해야 하는 메서드와 속성을 강제하는 역할을 합니다
  • extends
    • extends 키워드는 클래스 상속(inheritance)을 나타냅니다.
    • 클래스가 다른 클래스를 확장하거나 상속받을 때 사용됩니다.
    • 상속을 통해 부모 클래스의 속성과 메서드를 자식 클래스가 물려받을 수 있습니다.
    • 자식 클래스는 부모 클래스의 특성을 확장하여 사용할 수 있습니다.

transform( ) 메소드

  • 두 개의 파라미터를 가진다.
  • 첫번째 파라미터는 ==처리가 된 인자의 값이며,
  • 두번째 파라미터는 ==인자에 대한 메타 데이터를 포함한 객체이다.
  • 두번째 파라미터는 생략이 가능하다.
  • transform( ) 메소드 에서 return 된 값은 route 핸들러로 전해진다.
  • 예외가 발생하면, 클라이언트에 바로 전해진다.
transform(value: any, metadata: ArgumentMetadata)

console.log(value) //사용자가 입력한 값이 들어온다.
console.log(metadata) // 방금 들어온 데이터에 대한 정보
// {metatype : [Function:string], type: 'body', data: 'status'}

커스텀 파이프로 실제 기능을 구현하기

  • 구현 할 기능 : 상태(status)에 PUBLIC과 PRIVATE만 올 수 있게 해보자.\

Type ORM

TypeORM 이란.

  • typeORM은 node js 에서 실행됨
  • typescript 로 작성된 객체 관계형 매핑 라이브러리
  • my SQL, PostgreSQL, MariaDB, SQL lite, MS SQL Server, Oracle, SAP Hana및 웹 sql 과 같은 여러 데이터 베이스를 지원한다.

ORM

  • 객체와 관계형 데이터베이스의 데이터를 자동으로 변형 및 연결하는 작업이다.
  • 객체 지향 프로그래밍은 클래스를 사용
  • 관계형 데이터베이스는 테이블을 사용하여 데이터를 다루기 때문.
  • ORM을 이용한 개발은 객체와 데이터베이스의 변형에 유연하게 사용 할수 있다는 장점이 있다.
  • 만약 typeORM을 안쓴다면?
db.query('SELECT * FROM boards WHERE title = "Hello" AND status="PUBLIC", (err, result)=>{
    if(err){
        throw new Error('ERROR')
    }
}')

Type ORM의 이점

  • 모델을 기반으로 데이터베이스 테이블 체계를 자동으로 생성한다.
  • 데이터베이스에서 개체를 쉽게 삽입, 업데이트, 삭제를 할 숭 ㅣㅅ다.
  • 테이블간 매핑(관계설정)을 만든다.
  • 간단한 CLI 명령을 제공한다.
  • TypeORM은 간단한 코딩으로 ORM 프레임워크를 사용하기 쉽다.
  • 또한 다른 모듈과 쉽게 통합된다.

Type ORM 사용하기

 npm i pg typeorm @nestjs/typeorm --save

이후 src 폴더에 configs 폴더 만들고, 그 안에, typeorm.config.ts 파일 만들기.

typeorm.config.ts 파일

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const typeORMConfig: TypeOrmModuleOptions = {
    type: '',
    host: '',
    port: 3000,
    username: '',
    password: '',
    entities: [__dirname + '/../**/*.entity.{js, ts}'],
    //엔티티를 이렇게 설정하면 모든 엔티티를 포함한다는 말이다.
    synchronize: true,
};

루트모듈 === app.module.ts 파일임.
루트모듈에 추가한다.

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

엔티티 생성하기

board.entity.ts 파일

@Entity() //아래의 클래스는 엔티티 라고 알려주는것.
export class Board extends BaseEntity{
    @PrimaryGeneratedColumn()  //아래의 id라는 애가 board엔티티의 기본 키 열이에요
    id:number;

    @Column()  // Board 엔티티의 title및 description 과 같은 다른 열을 나타냄.
    title: string;

    @Column()
    description: string;

    @Column()
    status: BoardStatus;
}

3계층 아키텍처가 곧 repository pattern!

board.repository.ts 파일

import { EntityRepository, Repository } from "typeorm";
import { Board } from "./board.entity";

@EntityRepository(Board)  //클래스를 사용자 정의 custom 저장소로 선언할때 사용.
    export class BoardRepository extends Repository<Board>{
}

그다음 다른 곳에서도 이 레포지토리를 사용할 수 있도록
module 파일에서 import해준다.

board.module.ts

@Module({
    imports: [TypeOrmModule.forFeature([BoardRepository])],
    controllers: [BoardsController],
    providers: [BoardsService],
})