저번에 쓰던걸 이어서 작성 하겠다. 만약 저번에 쓰던게 필요하면 들어가서 읽어보도록 하자!
https://ohs020105.tistory.com/110
57일차 TIL
저번에 쓰던걸 이어서 써보겠다.저번에nest g co post // 컨트롤러 이렇게 적었던것 같은데 여기선 컨트롤러 툴밖에 안만들어준다. 추가로nest g s post // 이건 서비스nest g mo post // 이건 모듈이렇게 적
ohs020105.tistory.com
이번엔 직접 프로젝트를 진행을 하면서 좀 더 Nest.js에 익숙해지는 시간을 가져보자.
우선 우리가 만들어 볼건 간단한 게시글을 CRUD 할 수 있는 게시판을 만들어 볼 예정이다.

자 이렇게 초기화면이다.
여기서 명령어 를 입력해준다.
nest new .
이 명령어는 지금 현재 열려있는 폴더에 nest.js 프로젝트를 생성하겠다는 말이 된다.

그렇담 이렇게 어떤걸 사용할건지 물어본다.
우린 당연히 호환이 잘되는 npm을 이용해서 작업을 실행할 것 이다.
여기서 enter를 눌러서 실행하게 되면

이렇게 실행중 하면서 옆에 파일들이 생기고 있는걸 확인 할 수 가 있다.
설치가 완료가 되면

이렇게 터미널에 간단한 튜토리얼이 나오면서 설치가 완료가 된다.
*여기서 튜토리얼이란?
- 정말 친절하게도 nest가 설치된 파일 위치로 이동할수 있는 명령어를 알려주고 그 다음 서버를 실행할수 있는 명령어를 알려준다.
그런다음 우리는 따로 더 추가 해줄 것이 있다.
npm i @nestjs/mapped-types class-validator
이 명령어를 설치를 할 것인데,
여기서 @nestjs/mapped-types는 DTO 자체의 변환 및 상속을 도와주는 패키지 이며, class-validator는 DTO를 구성하는 데이터의 유효성을 검증하는 패키지 이다.
** 여기서 DTO는 뭔데??
간단하게 말하자면 Data Transfer Object의 약자로, 데이터를 전송하기 위해 작성된 객체라고 생각하면 편하다.
Nest.js 에서는 모든 데이터는 DTO를 통해서 운반되니 꼭 잊지 말자!
자 다시 본론으로 넘어와서 패키지 하나 더 설치를 해줘야 한다.
npm i lodash @types/lodash
lodash는 javaScript로 코딩할때 유용하게 사용할 수 있는 유틸성 패키지 이다.
만약 lodash에 대해서 좀 더 자세히 알고 싶으면 공식 문서를 한번 읽어보자.
Lodash
_.defaults({ 'a': 1 }, { 'a': 3, 'b': 2 });_.partition([1, 2, 3, 4], n => n % 2);DownloadLodash is released under the MIT license & supports modern environments. Review the build differences & pick one that’s right for you.InstallationIn
lodash.com
그런다음 다시 코드로 넘어가서

이 파일로 넘어가서 여기에 "esModuleInterop": true 이 문장을 추가해준다.
이 항목은 ES6 모듈 사양을 준수하여 CommonJS 모듈을 가져올 수 있게 해주는 것이다.

이렇게 추가해준다.
그런다음
cd src
라는 명령어를 입력해서 현재 파일 위치를 src로 이동하게 해준다.

그리고 제일 중요한것..! 이거때문에 Nest.js를 쓰는게 아닌가 싶을 정도로 작성자가 제일 감탄한 명령어가 있다...
nest g resource post
바로 이 명령어 인데 이 친구는 게시판의 REST API의 뼈대를 만들어 준다..!!!!

명령어를 입력하면 이렇게 선택할 수 있는데 우리는 api를 만들 것 이니 REST API를 선택해준다.

그럼 이걸 선택한게 맞냐? 지금 너가 있는 폴더위치(src)에다가 한다?
이렇게 물어본다. 당연히 yes.

그럼 이렇게 생성이 되었는데 파일 이름을 잠깐 확인해보자면 잠깐...
아키택쳐 패턴을 맞춰서 만들어준다..! 진짜 이건 혁명이다..

원래는 src 폴더에는 app관련해서 밖에 없는데 저 위에 밑줄친 곳을 보면 post라고 생겨있다.
이걸 누르게 되면

이렇게 생성이 되어 있는 모습을 볼 수가있다.
이 명령어 하나로 모듈이 만들어지고 모듈을 구성하는 DTO,엔티티,컨트롤러,서비스가 자동으로 만들어짐을 확인할 수 있다.
여기서 한번 작동이 잘 구동이 되는지 확인하기 위해서
npm run build
명령어를 입력해준다. 문제가 없으면 별다른 에러가 뜨지 않을 것 이다.

자 그럼 빌드도 완료했으니 한번 실행을 해보자.
npm run start
이 명령어를 터미널에 입력하면

이런 화면과 같이 작동이 잘 되는것을 확인 할 수가 있다.
이젠 여기선 아까 우리가 뼈대를 만든것을 실제 비즈니스 로직을 구현을 하면 된다.
우선 첫번째로 서비스를 구현해 볼것이다.
서비스 파일 (post.service.ts) 을 들어가게 되면

이렇게 뼈대만 만들어져 있는 것을 확인 할 수가 있다.
여기서 몇가지를 추가해 준다.
import _ from 'lodash';
import { Injectable } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
@Injectable()
export class PostService {
private articles: { id: number; title: string; content: string }[] = [];
private articlePasswords = new Map<number, number>();
create(createPostDto: CreatePostDto) {
return 'This action adds a new post';
}
findAll() {
return `This action returns all post`;
}
findOne(id: number) {
return `This action returns a #${id} post`;
}
update(id: number, updatePostDto: UpdatePostDto) {
return `This action updates a #${id} post`;
}
remove(id: number) {
return `This action removes a #${id} post`;
}
}
사실상 들어간건
import _ from 'lodash';
하고
@Injectable()
export class PostService {
private articles: { id: number; title: string; content: string }[] = [];
private articlePasswords = new Map<number, number>();
create(createPostDto: CreatePostDto) {
return 'This action adds a new post';
}
이렇게 변경된것 밖에 없다.
**참고로 여기선 데이터베이스를 사용하진 않을거라 저렇게 articles라는 변수로 게시물들을 담을 예정이다.**
**그럼담 articlePasswords 이 변수도 게시글과 게시글에 해당하는 비밀번호를 저장하는 변수이다.**
여기서 잠깐! @Injectable() 이 데코레이터의 의미를 모르는 사람은 밑에 글을 읽고 넘어가는걸 추천한다.
1. @Injectable()
이 데코레티너는 PostService 클래스가 NestJs의 의존성 주입 시스템에 의해 인스턴스화 될 수 있음을 나타낸다. 즉, 다른 클래스에서 이 서비스를 주입받아 사용할 수 있다.
2. export class PostService
PostService 라는 이름의 클래스를 정의하고, 이 클래스를 다른 모듈에서 사용할 수 있도록 내보낸다.
3.private articles
- articles라는 이름의 비공식 속성을 정의한다. 이 속성은 배열로, 각 요소는 객체 형태로 id, title, content 속성을 가진다. 초기값은 빈 배열이다.
* 타입: { id: number; title: string; content: string }[]는 이 배열이 특정 구조의 객체들로 구성되어 있음을 나타낸다.
4.priate articlePasswords
- priate articlePasswords 라는 이름의 비공식 속성을 정의한다. 이 속성은 map 객체로, 숫자키와 숫자 값을 가진다. 이는 각 게시물의 ID와 관련된 비밀번호를 저장하는 데 사용될 수 있다.
5. create(createPostDto: CreatePostDto)
- create라는 메서드를 정의한다. 이 메서드는 CreatePostDto 타입의 매개변수를 받아 새로운 게시물을 추가하는 기능을 수행 할 것이다.
*반환 값: 현재는 단순히 문자열 'this action adds a new post'를 반환한다. 실제 게시물을 생성하는 로직은 구현되어 있지 않다.
1. 이어서 이제 그 밑의 findAll의 비즈니스 로직을 채워보자.
findAll() {
return this.articles.map(({ id, title }) => ({ id, title }));
}
이걸 쉽게 풀어서 설명하자면
1. this.articles
- 이건 현재 클래스의 인스턴스를 참조한다는 것이다.
2. map()
- articles 배열의 각 게시물 객체에서 필요한 속성만 추출하여 새로운 배열을 만들기 위해 사용된다.
3. ({ id, title }) => ({ id, title })
- 화살표 함수로, 각 게시물 객체에서 id와 title 속성만을 추출하여 새로운 객체를 생성한다.
** ({ id, title })는 각 게시물 객체를 구조 분해 할당 하여 id와 title을 가져온다 **
2. 그 다음 findOne 함수를 구현 해보자.
findOne(id: number) {
return this.articles.find((article) => article.id === id);
}
역시나 간단하다! 그냥 articles 변수에서 해당 게시물의 ID를 기반으로 검색하여 게시물을 리턴만 하면 끝이다.
이제는 게시물을 만드는 create 함수를 만들려고 하는데 이 친구는 CreatePostDto라는 타입의 변수를 받게 되어있다.

으잉? 지금까지 Dto파일은 건든적이 없는데 이게 뭐지??
라고 생각할 수 있다. 이 파일은 쉽게 말해 검증을 하는 거라고 생각하면 될 것 같다.

처음에는 이렇게 비어있는데 이 내용을 채워보겠다 .
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreatePostDto {
@IsString()
@IsNotEmpty({ message: '게시물의 제목을 입력해주세요.' })
readonly title: string;
@IsString()
@IsNotEmpty({ message: '게시물의 내용을 입력해주세요.' })
readonly content: string;
@IsNumber()
@IsNotEmpty({ message: '게시물의 비밀번호를 입력해주세요.' })
readonly password: number;
}

채우면 이렇게 되는데
여기서 처음 보는 생소한 단어가 있다...
@IsString() 과, @IsNotEmpty 가 있다. 이 데코레티너는 무엇을 하는걸까?
쉽게 생각하면 DTO를 구성하는 각각의 데이터 필드의 타입을 정의하는하는 것이다.
-예를 들자면 , title의 경우 @isString으로 선언되어 있는걸 확인 할 수 있다. 이 경우에는 제목을 문자열로 전달하지 않고 숫자나 다른 값으로 전달을 하면 유효하지 않게 되고 에러 응답을 받게 된다.
- @IsNotEmpty라는 데코레이터는 이 필드는 꼭 전달을 해줘야 한다!!! 라는 의미를 가지고 있다. 전달하지 않으면 유효하지 않게 되고 에러 응답을 받는다.
- DTO 파일에서는 대부분의 데이터 필드에는 readonly 키워드를 붙여주는 것이 좋다. 왜냐면, DTO 객체의 속성 값은 생성 후에 변경될 필요가 없기 때문에 해당 속성이 변경되지 않도록 하는 것이 좋은 방법이다.
3. 그 다음으로는 create 함수를 작성해보겠다.
create(createPostDto: CreatePostDto) {
const { title, content, password } = createPostDto;
const id = this.articles.length + 1;
this.articles.push({ id, title, content });
this.articlePasswords.set(id, password);
return { id };
}
여기서 ID는 1부터 시작해서 계속 증가해야 한다. 따라서, 현재 articles의 길이에 1을 더해줘야 한다. 왜냐하면, 최초의 길이는 0일테니까.
이후에 각각의 변수에 해당하는 데이터들을 저장하고 ID를 리턴하면 끝이다.
**왜 ID를 리턴하는게 좋을까?
예를 들어서 사용자가 게시글을 작성한 후 해당 게시글의 상세 페이지로 리다이렉션하거나, 추가 작업을 수행하기 위해 생성된 게시글의 ID가 필요할 수 있기 때문이다.
4. 이제 update 함수를 만들어야 되는데 이 친구도 create함수와 마찬가지로 DTO를 받고 있어서 먼저 UpdatePostDto 파일을 만들어 주겠다.
import {OmitType, PickType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';
export class UpdatePostDto extends OmitType(CreatePostDto, ['title']) {}
이렇게 DTO 파일을 수정해줬으면 바로 update 함수를 변경해보겠다.
update(id: number, updatePostDto: UpdatePostDto) {
const { content, password } = updatePostDto;
const article = this.articles.find((article) => article.id === id);
if (_.isNil(article)) {
throw new NotFoundException('게시물을 찾을 수 없습니다.');
}
const articlePassword = this.articlePasswords.get(id);
if (!_.isNil(articlePassword) && articlePassword !== password) {
throw new NotFoundException('비밀번호가 일치하지 않습니다.');
}
article.content = content;
}
post.service.ts 파일의 import문에서 참조하는 lodash를 사용하여 _.isNil이라는 함수를 부르고 해당 함수로 article의 NULL 여부를 체크 하고 있다.
물론 , !article도 사용될 수 있지만 !articled은 falsy 값일때 true를 넘겨준다. 따라서 _.isNil을 선호하는 편이다.
- NotFoundException이라는 예외는 Nest.js에서 제공하는 많은 내장 예외 중 하나입니다! 이렇게 되면 코드의 가독성도 올라가고 표준화된 응답을 제공할 수 있어요!
- 비밀번호 체크는 반드시 필수입니다! 비밀번호가 저장이 안되어있다면 상관이 없지만 저장이 되어있다면 무조건 비밀번호 체크를 해야겠죠!
- 하지만, 여기서도 NotFoundException 예외를 던지는게 맞는 선택일까요? 이것은 TypeORM 강의에서 다시 확인해보시죠!
- ID는 굳이 리턴을 할 필요가 없어요. 왜냐하면 클라이언트에서 이미 게시물의 ID 값을 알고 있는데다가 이 ID는 업데이트가 된다고 변경되지 않으니까요.
5. 이제 마지막기능인 remove 함수를 구현하기 전에 우선 remove도 dto가 필요하니 파일을 하나 만들어 준다.
import { PickType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';
export class RemovePostDTO extends PickType(CreatePostDto, ['password']) {}
이제 그 뒤에 함수를 적어주면 된다.
remove(id: number, deletePostDto: RemovePostDTO) {
const articleIndex = this.articles.findIndex(
(article) => article.id === id,
);
if (articleIndex === -1) {
throw new NotFoundException('게시물을 찾을 수 없습니다.');
}
const articlePassword = this.articlePasswords.get(id);
if (
!_.isNil(articlePassword) &&
articlePassword !== deletePostDto.password
) {
throw new NotFoundException('비밀번호가 일치하지 않습니다.');
}
this.articles.splice(articleIndex, 1);
}
이렇게 하면 서비스 에서 할 로직은 끝이 나게 된다.
그다음에는 컨트롤러로 가서 로직들을 확인해야 한다.
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
} from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { RemovePostDTO } from './dto/remove-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostService } from './post.service';
@Controller('post')
export class PostController {
constructor(private readonly postService: PostService) {}
@Post()
create(@Body() createPostDto: CreatePostDto) {
return this.postService.create(createPostDto);
}
@Get()
findAll() {
return this.postService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.postService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
return this.postService.update(+id, updatePostDto);
}
@Delete(':id')
remove(@Param('id') id: string, @Body() deletePostDto: RemovePostDTO) {
return this.postService.remove(+id, deletePostDto);
}
}
초기에 생성된 코드랑 크게 다를게 없지만 딱 하나 달라진게 있다 .그건 바로 delete에 RemovePostDTO 타입의 인자를 추가로 던지다는 것이다.
- @Controller('post') 데코레이터
*서버주소에 /post를 붙여넣어서 api를 호출하면 (e.g.http://서버주소/post이 컨트롤러가 요청을 담당하겠다는 의미이다. 즉 , route 정보라고 생각하면 된다!
- constructor(private readonly postService: PostService) {}의 의미가 이해가 가지 않는다면 이전 강의에 있었던 IoC와 DI에 대해서 다시 공부해보도록 하세요!
- @Get, @Patch나 @Delete를 보면 공통적으로 :id라는 인자가 있습니다. 이것은 URL 파라미터라고 하며 코드에서 볼 수 있듯이 @Param이라는 데코레이터로 값을 받아올 수 있습니다.
- e.g. GET <http://서버주소/post/1>
- post 뒤에 붙는 1이 파라미터이며 위에서 findOne함수가 이를 받아서 처리합니다.
- e.g2. PATCH <http://서버주소/post/5>
- post 뒤에 붙는 5가 파라미터이며 위에서는 update 함수가 이를 받아서 처리합니다.
- e.g. GET <http://서버주소/post/1>
- GET을 제외한 나머지 REST API들은 DTO를 통해서 데이터를 전달해야 한다고 했었어요. 이 때, @Body라는 데코레이터를 사용해서 DTO를 사용자로부터 전달받으면 됩니다!
- 엄밀히 말하자면, DELETE 역시 DTO는 필요 없습니다만 여기서는 예외입니다.
-
더보기
🤔 URL 파라미터는 id인데 서비스에 전달을 할 때는 왜 +id로 전달을 하죠?
→ +id는 피연산자 즉, +(연산자) 뒤에 붙는 id(피연산자)를 숫자로 변환하는 JavaScript의 트릭입니다. 따라서, 자동으로 생성된 컨트롤러에서는 이렇게 코드가 생성이 되는 것이죠!