목차
I. Module
•
@Module() 데코레이터로 주석이 달린 클래스
•
각 애플리케이션에는 하나 이상의 모듈(루트 모듈)이 있어야 함
•
밀접하게 관련된 기능 집합을 구성 요소를 구성 (기능 별로 모듈을 생성함)
- Module 생성
nest g module boards
Shell
복사
•
nest: using nestcli
•
g: generate
•
module: schematic that I want to create
•
boards: name of the schematic
II. Controller
•
들어오는 요청을 처리하고 클라이언트에 응답을 반환
•
컨트롤러는 @Controller 데코레이터로 클래스를 데코레이션하여 정의됨
◦
데코레이터는 인자를 Controller에 의해 처리되는 “경로”로 받음
- Handler
•
@Get, @Post, @Delete 등과 같은 데코레이터로 장식된 컨트롤러 클래스 내의 단순 메서드
- Controller 생성
nest g controller boards --no-spec
Shell
복사
•
nest: using nestcli
•
g: generate
•
controller: controller schematic
•
boards: name of the schematic
•
--no-spec: 테스트를 위한 소스 코드를 생성하지 않음 (쓰지 않으면 테스트 코드가 생성됨)
- Contoller 생성 과정
1.
cli는 먼저 boards 폴더를 찾음
2.
boards 폴더 안에 controller 파일 생성
3.
boards 폴더 안에 module 파일 찾기
4.
mudule 파일 안에 controller 넣기
III. Providers
•
대부분의 기본 Nest 클래스는 서비스, 리포지토리, 팩토리, 헬퍼 등 프로바이더로 취급될 수 있음
•
프로바이더의 주요 아이디어는 종속성으로 주입할 수 있다는 것
◦
즉, 객체는 서로 다양한 관계를 만들 수 있으며 객체의 인스턴스를 “연결”하는 기능은 대부분 Nest 런타임 시스템에 위임될 수 있음
•
컨트롤러에서 모든 필요 기능을 구현할 수 없음
◦
서비스, 리포지토리 등이 필요
•
컨트롤러에서 사용할 수 있도록 주입하여 사용
•
Provider를 사용하기 위해서는 Module에 등록해주어야 사용할 수 있음
IV. Service
•
소프트웨어 개발 내의 공통 개넘
•
@Injectable 데코레이터로 감싸져 모듈에 제공된 서비스 인스턴스는 애플리케이션 전체에서 사용될 수 있음
•
컨트롤러에서 데이터의 유효성 체크를 하거나 데이터베이스에 아이템을 생성하는 등의 작업을 하는 부분을 처리함
- Service를 Controller에서 이용할 수 있는 방법 (Dependency Injection)
•
Service를 Constructor 클래스에서 가져옴(Injected)
•
접근제한자를 통해 생성자 파라미터를 선언하면 Controller 안에서 사용할 수 있음
◦
타입스크립트의 기능을 이용해 종속성을 타입으로 해결할 수 있음
- Board Service 생성
nest g service boards --no-spec
Shell
복사
•
Service 안에서는 데이터베이스 관련 로직 처리
•
데이터베이스에서 데이터를 가져오거나 데이터베이스 안에 게시판 생성할 때 생성한 게시판 정보를 넣어주는 등의 로직 처리
- Board Service를 Board Controller에서 이용할 수 있게 해주기 (Dependency Injection)
•
종속성 주입은 클래스의 Constructor 안에서 이루어짐
•
JS에서는 private 같은 접근 제한자를 사용할 수 없지만 TS에서는 가능
•
접근 제한자(public, protected, private)를 생성자 파라미터에 선언하면 접근 제한자가 사용된 생성자 파라미터는 암묵적으로 클래스 프로퍼티로 선언됨
@Controller('boards')
export class BoardsController {
boardsService: BoardsService; // 3
constructor(boardsService: BoardsService) { // 1
this.boardsServie = boardsService; // 2
}
}
TypeScript
복사
→
@Controller('boards')
export class BoardsController {
boardsService: BoardsService;
constructor(private boardsService: BoardsService) {}
}
TypeScript
복사
1.
boardsService 파라미터에 BoardsService 객체를 타입으로 지정해줌
2.
이 boardsService 파라미터를 BoardsController 클래스 안에서 사용하기 위해 this.boardsService 프로퍼티에 boardsService 파라미터를 할당해줌
3.
하지만 타입스크립트에서는 선언한 값만 객체의 프로퍼티로 사용가능하기 때문에 boardsService: BoardsService로 선언해줌
4.
이렇게 갖게 된 boardsService 프로퍼티를 이용해 BoardsController 클래스 안에서 활용 가능
V. 로직 구현
I. CRUD
I. 모든 게시물 가져오는 서비스 만들기 (CRUD → R)
모든 게시물 데이터를 데이터베이스에서 가져오는 로직 구현
•
우선은 데이터를 로컬 메모리에 담아 처리
•
클라이언트에서 요청을 보내면 먼저 컨트롤러로 가며 컨트롤러에서 알맞은 요청 경로에 라우팅해서 해당 핸들러로 가게 해줌
•
요청을 처리해주기 위해 서비스로 들어가며 그 요청에 맞는 로직을 서비스에서 처리해준 후 컨트롤러에 리턴값을 보내준 후 컨트롤러에서 클라이언트로 결과값을 보내줌
•
컨트롤러에서는 요청을 처리하고 결과값을 리턴해주는 역할을 함
◦
Controller → handle the requests
Service
II. Board Model 정의
•
게시물 생성 기능 만들기 전, 게시물에 필요한 데이터가 어떤 것이 필요한지를 정의해주기 위해 게시물의 모델 생성
◦
ex) ID, 이름, 설명 등
◦
board Model 파일 생성 → board.model.ts
•
모델을 정의하기 위해서는 Class나 Interface를 사용
◦
Interface - 변수의 타입만을 체크
◦
Class - 변수의 타입 체크 + 인스턴스 생성
•
board의 구조만 정의하기 위해 interface 사용
export interface Board {
id: string;
title: string;
description: string;
status: BoardStatus;
}
TypeScript
복사
◦
status는 공개 게시물인지 비공개 게시물인지 나눠주는 것
▪
이 두가지 상태 이외에는 나오면 안되기 때문에 enumeration을 사용
export enum BoardStatus {
PUBLIC = 'PUBLIC',
PRIVATE = 'PRIVATE',
}
TypeScript
복사
•
Service와 Controller 핸들러에서 결과값을 리턴하는 부분도 타입을 정의해줌 (:Board[])
•
타입을 정의해주는 것은 선택사항이지만, 타입을 정의해줌으로서 원하는 타입과 다른 코드를 사용할 시 에러를 발생시키게 하고, 코드를 읽는 입장에서 이해가 더 쉬움
III. 게시물 생성 기능 만들기 (CRUD → C)
I. 게시물 생성 Service
•
게시물에 관한 로직처리는 Service에서 이뤄짐
•
따라서 Service에서 로직을 처리 후 Controller에서 서비스를 불러옴
◦
Service → Controller
•
게시물의 ID는 유니크해야함
◦
데이터베이스에 데이터를 넣어줄 때는 데이터베이스가 알아서 유니크한 값을 줌
◦
현재는 데이터베이스를 사용하지 않고 로컬메모리를 사용하기 때문에 임의로 값을 줌
◦
여러 방법이 있는데 uuid 모듈을 이용해 유니크한 값을 줌
▪
uuid를 설치하여 사용
•
npm install uuid --save
•
uuid 모듈을 사용하기 원하는 곳에 import
•
import { v1 as uuid } from ‘uuid’;
II. 게시물 생성 Controller
•
로직 부분을 처리 했다면 Request와 Response 부분 처리
•
클라이언트에서 보내온 값들을 핸들러에 보내기 위해 NestJS에서는 @Body body를 이용함
◦
하나씩 가져오려면 @Body(’title’) title 또는 @Body(’description’) description 을 사용
•
Request에서 보내온 값들을 Controller에서 받아오면 이 값들을 Service에서 로직 처리를 하기 위해 param으로 넘겨줌
IV. ID로 특정 게시물 가져오기 (CRUD → R)
Service
•
find → 배열에서 특정 요소를 찾기 위한 메소드
Controller
•
@Body 처럼 @Param 을 사용
◦
localhost:3000?id=feiuaniudfisaf ← 이런 경우 Param을 사용
◦
전체를 가져오는 경우 - @Param() params: string[]
◦
특정 부분만 가져오는 경우 - @Param(’id’) id: string
V. ID로 특정 게시물 삭제하기 (CRUD → D)
Service
•
filter - id가 같지 않은 것만 남기고 같은 건 삭제함
•
클라이언트에 return 값을 주지 않아도 됨 (삭제이기 때문, 이건 필요에 따라 다름)
VI. 특정 게시물의 상태 업데이트 (CRUD → U)
Service
•
id를 통해 특정 게시물의 정보를 가져옴
•
status를 변경하여 반환
Controller
•
Param과 Body를 통해 id와 status를 파라미터로 입력
II. DTO (Data Transfer Object)
•
계층간 데이터 교환을 위한 객체
•
DB에서 데이터를 얻어 Service나 Controller 등으로 보낼 때 사용하는 객체를 말함
•
DTO는 데이터가 네트워크를 통해 전송되는 방법을 정의하는 객체
•
interface나 class를 이용해 정의될 수 있음 (Class를 이용하는 것을 Nest JS에서는 권장함)
- DTO 사용 이유
•
데이터 유효성을 체크하는데 효율적임
•
더 안정적인 코드 사용이 가능하며, 타입스크립트의 타입으로도 사용됨
•
애플리케이션의 유지보수를 위함
◦
Board를 위한 Property(title, description, …)들을 여러 곳에서 사용하고 있는데
◦
한 곳에서 Property의 이름을 바꿔줘야한다면 다른 곳에 똑같이 쓰인 곳의 프로퍼티도 바꿔야 함
◦
이를 방지하기 위해 DTO 사용
- 게시물 생성을 위한 DTO 생성
DTO 파일 생성
•
클래스는 인터페이스와 다르게 런타임에서 작동하기 때문에 파이프 같은 기능을 이용할 때 더 유용함
DTO 적용
•
Controller와 Service에 DTO 적용
III. 예외 처리
I. 특정 게시물을 찾을 때 없는 경우 결과 값 처리
•
ID로 게시물을 찾을 때 없는 아이디의 게시물을 찾으면 클라이언트에게 에러 메시지 반환
•
예외 인스턴스 사용
◦
NotFoundException()
•
에러 메시지를 설정하기 위해서는 NotFoundException() 파라미터로 메시지 추가
◦
NotFoundException(`Can't find Board with id ${id}`)
▪
작은따옴표 ‘’ 아님, 백틱(backtick)임 ``
II. 없는 게시물 지우려 할 때 결과 값 처리
•
없는 게시물을 지우려할 때 에러 값 반환
•
이미 있는 메소드인 getBoardById를 이용해서 지우려는 게시물이 있는지 체크 후 없다면 에러 문구 반환
IV. Entity
•
VII. TypeORM 에서 이어짐
•
데이터베이스에 데이터를 직접 SQL 형식에 맞춰 생성하지 않아도 됨
•
TypeORM을 사용하면 데이터베이스 테이블로 Class가 변환되기 때문에 클래스를 생성한 후 컬럼을 정의하면 됨
데코레이터
•
@Entity()
◦
Board 클래스가 엔티티임을 나타내는데 사용됨
◦
CREATE TABLE board 부분
•
@PrimearyGeneratedColumn()
◦
id 열이 Board 엔티티의 기본 키 열임을 나타내는데 사용
•
@Column()
◦
Board 엔티티의 title 및 description과 같은 다른 열을 나타내는데 사용
V. Repository
•
엔티티 개체와 함께 작동하며 엔티티 찾기, 삽입, 업데이트, 삭제 등을 처리함
•
데이터베이스에 관련된 일은 서비스에서 하는게 아닌 Repository에서 해줌 → Repository Pattern
◦
INSERT, FIND, DELETE.. 등
Repository 생성
1.
리포지토리 파일 생성
2.
생성 파일에 리포지토리를 위한 클래스 생성
•
생성 시 Repository 클래스를 Extends 해줌
◦
Find, Insert, Delete 등 엔티티를 컨트롤 해줄 수 있음
•
@EntityRepository()
◦
클래스를 사용자 정의 저장소로 선언하는데 사용
◦
사용자 지정 저장소는 일부 특정 엔티티를 관리하거나 일반 저장소일 수 있음
3.
생성한 Repository를 다른 곳에서도 사용할 수 있게 하기 위해(injectable) board.module에서 import 시킴
** 버전 변경으로 인한 코드 수정
•
@EntityRepository() 는 더이상 사용되지 않는 코드이므로 변경이 필요함
1.
Repository 파일 수정
// user.repository.ts
@Injectable()
export class UsersRepository extends Repository<UsersEntity> {
constructor(private dataSource: DataSource) {
super(UsersEntity, dataSource.createEntityManager());
}
async getById(id: string) {
return this.findOne({ where: { id } });
}
// ...
}
TypeScript
복사
2.
서비스에 레포지토리 주입(Inject)
// user.service.ts
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async getById(id: string): Promise<User> {
return this.userRepository.getById(id);
}
// ...
}
TypeScript
복사
3.
모듈에 forFeature 값으로 Entity를 갖고, provider에는 Repository 추가
// user.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([UserEntity])],
// ...
],
providers: [UserService, UserRepository],
// ...
})
export class UserModule {}
TypeScript
복사
VI. Pipes
•
파이프는 @Injectable () 데코레이터로 주석이 달린 클래스
•
파이프는 data transformation과 data validation을 위해 사용됨
•
컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동함
•
Nest는 메소드가 호출되기 직전에 파이프를 삽입하고 파이프는 메소드로 향하는 인수를 수신하고 이에 대해 작동함
Data Transformation
•
입력 데이터를 원하는 형식으로 변환
◦
ex) String to Integer
Data validation
•
입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달
•
유효하지 않은 경우 예외를 발생
◦
ex) 이름의 길이가 10자 이하여아 하는데 10자 이상인 경우 에러 발생
파이프는 위에 두가지 모든 경우에서 라우트 핸들러가 처리하는 인수에 대해 작동함
파이프는 메소드를 바로 직전에 작동해서 메소드로 향하는 인수에 대해 변환과 유효성 체크를 위해 호출됨
- Pipe 사용하는 법 (Binding Pipes)
I. Handler-level Pipes
•
핸들러 레벨에서 @UsePipes() 데코레이터를 이용해 사용
•
모든 파라미터에 적용됨
II. Parameter-level Pipes
•
파라미터 레벨의 파이프
•
특정 파라미터에만 적용되는 파이프
III. Global-level Pipes
•
애플리케이션 레벨의 파이프
•
클라이언트에서 들어오는 모든 요청에 적용
•
가장 상단 영역인 main.ts에 넣어주면 됨
* Built-in Pipes
•
NestJS에 기본적으로 사용할 수 있게 만들어 놓은 6가지 파이프
1.
ValidationPipe
2.
ParseIntPipe
3.
ParseBoolPipe
4.
ParseArrayPipe
5.
ParseUUIDPidpe
6.
DefaultValuePipe
- Pipe를 이용한 유효성 체크
I. 게시물 생성 시
•
title과 description의 값이 없으면 생성되지 않도록 수정
필요 모듈
•
class-validator, class-transformer
◦
npm install class-validator class-transformer --save
- 커스텀 파이프를 이용한 유효성 체크
•
PipeTransform이란 인터페이스를 새롭게 만들 커스텀 파이프에 구현해줘야 함 (implements PipeTransform)
•
PipeTransform 인터페이스는 모든 파이프에서 구현해줘야하는 인터페이스
•
모든 파이프는 transform() 메소드를 필요로 함
◦
NestJS가 인자를 처리하기 위해 사용됨
transform() 메소드
•
두개의 파라미터를 가짐
1.
처리가 된 인자의 값
2.
인자에 대한 메타 데이터를 포함한 객체
•
transform() 메소드에서 return된 값은 Route Handler로 전해짐
•
예외가 발생하면 클라이언트에 바로 전해짐
I. 상태값 유효성 체크
•
상태값이 PUBLIC, PRIVATE가 아닌 경우 에러 발생
VII. TypeORM
•
node.js에서 실행되고 TypsScript로 작성된 객체 관계형 매퍼 라이브러리
•
MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SQP Hana 및 WebSQL과 같은 여러 데이터베이스를 지원함
•
Object Relational Mapping
◦
객체와 관계형 데이터베이스의 데이터를 자동으로 변형 및 연결하는 작업
◦
ORM을 이용한 개발은 객체와 데이터 베이스의 변형에 유연하게 사용할 수 있음
▪
객체(클래스) ← 매핑 → 관계형 Database(테이블)
•
객체 모델과 관계형 모델 간 불일치를 해결
TypeORM vs Pure JavaScript
// TypeORM
const boards = Board.find({title: 'Hello', status: 'PUBLIC'});
// Pure JavaScript
db.query('SELECT * FROM boards WHERE title = "Hello" AND status = "PUBLIC", (err, result) => {
if (err) {
throw new Error('Error')
}
boards = result.rows;
})
TypeScript
복사
TypeORM
•
모델을 기반으로 데이터베이스 테이블 체계를 자동으로 생성
•
데이터베이스에서 개체를 쉽게 삽입, 업데이트 및 삭제 가능
•
테이블 간의 매핑 (일대일, 일대다 및 다대다)을 만듦
•
간단한 CLI 명령을 제공
•
간단한 코딩으로 ORM 프레임워크를 사용하기 쉬움
•
다른 모듈과 쉽게 통합 가능
TypeORM 사용
설치 모듈
•
@nestjs/typeorm
◦
NestJS에서 TypeORM을 사용하기 위해 연동시켜주는 모듈
•
typeorm
◦
TypeORM 모듈
•
pg
◦
Postgres 모듈
npm install pg typeorm @nestjs/typeorm --save
TypeORM 애플리케이션에 연결
1.
typeORM 설정파일 생성
2.
typeORM 설정파일 작성
•
synchronize true는 production 모드에서는 false로 해야 함
◦
그렇지 않을 시 데이터를 잃을 수 있음
◦
true 값을 주면 애플리케이션을 다시 실행할 때 엔티티 안에서 수정된 컬럼의 길이, 타입, 변경값 등을 해당 테이블을 Drop한 후 다시 생성해줌
•
entities는 나중에 생성할 엔티티 하나씩 넣어줄 수도 있지만 모든 엔티티를 다 포함하게 할 수 있음
◦
하나씩 작성도 가능
3.
루트 Moduel에서 Import
•
app.moduel.ts
◦
TypeOrmModule.forRoot(typeORMConfig)
▪
for Root안에 넣어준 설정(configuration)은 모든 부수적인 모듈들에 적용됨
VIII. 로직 수정
- 코드 재설정
•
← 데이터베이스가 아닌 로컬 메모리를 사용했기 때문에 코드 재설정
1.
Service와 Controller의 기존 코드 주석 처리
2.
메모리에 데이터 저장을 하지 않기 때문에 Service의 board 배열 삭제
3.
게시물 데이터를 정의하는데 Entity를 사용하기 때문에 Board Model 파일의 Board Interface 삭제
4.
Status Enum은 사용하기에 파일 생성하여 삽입
a.
board.model.ts 파일 삭제 후 board-status.enum.ts 생성
5.
데이터베이스 이용으로 인한 불필요 경로 삭제
a.
board-status-validation.pipe.ts BoardStatus
b.
boards.controller.ts
c.
board.entity.ts BoardStatus
d.
uuid
I. ID를 사용한 특정 게시물 가져오기 (CRUD → R)
•
메모리에서 데이터를 가져오는게 아닌 데이터베이스에서 가져옴
•
TypeORM을 사용할 때는 Repository Pattern을 사용
◦
Service에 Repository를 넣어줌(Inject)
▪
@InjectRepository - 데코레이터를 통해 해당 서비스에서 repository를 이용
getBoardById
Service
•
typeOrm에서 제공하는 findOne 메소드 이용
•
async await을 통해 데이터베이스 작업이 끝난 후 결과값을 받을 수 있게 해줌
◦
데이터베이스 요청 처리가 되고 처리가 완료된 결과를 받음
◦
async await이 없으면 처리 중인 상태를 받기 때문에 에러가 남
II. 게시물 생성하기 (CRUD → C)
Service
•
typeOrm의 create 메소드를 통해 생성
•
데이터베이스에 저장하기 위해 await, save 사용
III. 게시물 삭제하기 (CRUD → D)
remove() vs delete()
•
remove - 존재하지 않는 아이템을 지우고자 하면 에러 발생 (404 Error)
•
delete - 아이템이 존재하면 지우고, 존재하지 않으면 아무 영향 없음
•
이 때문에 remove를 사용하면 (아이템 유무 + 지우기) 두번 데이터베이스를 이용해야 해서 delete 메소드를 사용하는게 효율적임
IV. 게시물 상태 업데이트하기 (CRUD → U)
•
기존과 같음
•
V. 모든 게시물 가져오기 (CRUD → R)
•
typeOrm의 find 메소드를 사용